diff --git a/Dockerfile b/Dockerfile index 1229b56..af798d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,5 @@ ARG git_sha="development" ENV GIT_SHA=$git_sha # Start the server with uvicorn -ENTRYPOINT ["poetry", "run"] -CMD ["uvicorn", "backend:app", "--host", "0.0.0.0", "--port", "8000"] +ENTRYPOINT ["/bin/bash", "-c"] +CMD ["poetry run alembic upgrade head && poetry run uvicorn backend:app --host 0.0.0.0 --port 8000"] diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..89c7735 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,53 @@ +# A generic, single database configuration. + +[alembic] +script_location = migrations +file_template = %%(epoch)s-%%(rev)s_%%(slug)s +prepend_sys_path = . +timezone = utc +version_path_separator = os +output_encoding = utf-8 + +[post_write_hooks] +hooks = ruff-lint, ruff-format +ruff-lint.type = exec +ruff-lint.executable = ruff +ruff-lint.options = check --fix-only REVISION_SCRIPT_FILENAME + +ruff-format.type = exec +ruff-format.executable = ruff +ruff-format.options = format REVISION_SCRIPT_FILENAME + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/__init__.py b/backend/__init__.py index c2e1335..eb276c0 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -1,3 +1,6 @@ +import asyncio +import os + import sentry_sdk from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from starlette.applications import Starlette @@ -14,6 +17,10 @@ from backend.route_manager import create_route_map from backend.validation import api +# On Windows, the selector event loop is required for psycopg. +if os.name == "nt": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + ORIGINS = [ r"(https://[^.?#]*--pydis-forms\.netlify\.app)", # Netlify Previews r"(https?://[^.?#]*.forms-frontend.pages.dev)", # Cloudflare Previews diff --git a/backend/authentication/user.py b/backend/authentication/user.py index 5e99546..6ad4c63 100644 --- a/backend/authentication/user.py +++ b/backend/authentication/user.py @@ -4,8 +4,9 @@ from pymongo.database import Database from starlette.authentication import BaseUser -from backend import discord, models +from backend import discord from backend.constants import SECRET_KEY +from backend.models import dtos class User(BaseUser): @@ -15,7 +16,7 @@ def __init__( self, token: str, payload: dict[str, t.Any], - member: models.DiscordMember | None, + member: dtos.DiscordMember | None, ) -> None: self.token = token self.payload = payload diff --git a/backend/constants.py b/backend/constants.py index 1e55cd2..4bb051c 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -4,17 +4,24 @@ from dotenv import load_dotenv from redis.asyncio import Redis as _Redis +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine load_dotenv() FRONTEND_URL = os.getenv("FRONTEND_URL", "https://forms.pythondiscord.com") -DATABASE_URL = os.getenv("DATABASE_URL") -MONGO_DATABASE = os.getenv("MONGO_DATABASE", "pydis_forms") SNEKBOX_URL = os.getenv("SNEKBOX_URL", "http://snekbox.default.svc.cluster.local/eval") REDIS_CLIENT = _Redis.from_url(os.getenv("REDIS_URL"), encoding="utf-8") +MONGO_DATABASE = os.getenv("MONGO_DATABASE", "pydis_forms") +MONGO_DATABASE_URL = os.getenv("MONGO_DATABASE_URL") + +PSQL_DATABASE_URL = os.getenv("PSQL_DATABASE_URL") +DATABASE_ECHO = os.getenv("DATABASE_ECHO", "false").lower() == "true" +_DB_ENGINE = create_async_engine(PSQL_DATABASE_URL, echo=DATABASE_ECHO) +DB_SESSION_MAKER = async_sessionmaker(_DB_ENGINE) + PRODUCTION = os.getenv("PRODUCTION", "True").lower() != "false" PRODUCTION_URL = "https://forms.pythondiscord.com" @@ -49,6 +56,12 @@ "vote", ] + +class TextType(Enum): + SHORT_TEXT = "short_text" + TEXT_AREA = "text_area" + + REQUIRED_QUESTION_TYPE_DATA = { "radio": { "options": list, diff --git a/backend/discord.py b/backend/discord.py index 4a1ecf5..22826e2 100644 --- a/backend/discord.py +++ b/backend/discord.py @@ -6,7 +6,8 @@ import starlette.requests from starlette import exceptions -from backend import constants, models +from backend import constants +from backend.models import dtos async def fetch_bearer_token(code: str, redirect: str, *, refresh: bool) -> dict: @@ -51,7 +52,7 @@ async def fetch_user_details(bearer_token: str) -> dict: return r.json() -async def _get_role_info() -> list[models.DiscordRole]: +async def _get_role_info() -> list[dtos.DiscordRole]: """Get information about the roles in the configured guild.""" async with httpx.AsyncClient() as client: r = await client.get( @@ -60,13 +61,13 @@ async def _get_role_info() -> list[models.DiscordRole]: ) r.raise_for_status() - return [models.DiscordRole(**role) for role in r.json()] + return [dtos.DiscordRole(**role) for role in r.json()] async def get_roles( *, force_refresh: bool = False, -) -> list[models.DiscordRole]: +) -> list[dtos.DiscordRole]: """ Get a list of all roles from the cache, or discord API if not available. @@ -77,7 +78,7 @@ async def get_roles( roles = await constants.REDIS_CLIENT.hgetall(role_cache_key) if roles: return [ - models.DiscordRole(**json.loads(role_data)) for role_id, role_data in roles.items() + dtos.DiscordRole(**json.loads(role_data)) for role_id, role_data in roles.items() ] roles = await _get_role_info() @@ -86,7 +87,7 @@ async def get_roles( return roles -async def _fetch_member_api(member_id: str) -> models.DiscordMember | None: +async def _fetch_member_api(member_id: str) -> dtos.DiscordMember | None: """Get a member by ID from the configured guild using the discord API.""" async with httpx.AsyncClient() as client: r = await client.get( @@ -99,14 +100,14 @@ async def _fetch_member_api(member_id: str) -> models.DiscordMember | None: return None r.raise_for_status() - return models.DiscordMember(**r.json()) + return dtos.DiscordMember(**r.json()) async def get_member( user_id: str, *, force_refresh: bool = False, -) -> models.DiscordMember | None: +) -> dtos.DiscordMember | None: """ Get a member from the cache, or from the discord API. @@ -118,7 +119,7 @@ async def get_member( if not force_refresh: result = await constants.REDIS_CLIENT.get(member_key) if result: - return models.DiscordMember(**json.loads(result)) + return dtos.DiscordMember(**json.loads(result)) member = await _fetch_member_api(user_id) if member: @@ -150,14 +151,14 @@ async def _verify_access_helper( if "admin" in request.auth.scopes: return - form = models.Form(**form) + form = dtos.Form(**form) for role_id in getattr(form, attribute, None) or []: role = await request.state.db.roles.find_one({"id": role_id}) if not role: continue - role = models.DiscordRole(**json.loads(role["data"])) + role = dtos.DiscordRole(**json.loads(role["data"])) if role.name in request.auth.scopes: return diff --git a/backend/middleware.py b/backend/middleware.py index 0b08859..5b36473 100644 --- a/backend/middleware.py +++ b/backend/middleware.py @@ -3,7 +3,7 @@ from starlette.responses import JSONResponse from starlette.types import ASGIApp, Receive, Scope, Send -from backend.constants import DATABASE_URL, DOCS_PASSWORD, MONGO_DATABASE +from backend.constants import DB_SESSION_MAKER, DOCS_PASSWORD, MONGO_DATABASE, MONGO_DATABASE_URL class DatabaseMiddleware: @@ -12,12 +12,14 @@ def __init__(self, app: ASGIApp) -> None: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: client: AsyncIOMotorClient = AsyncIOMotorClient( - DATABASE_URL, + MONGO_DATABASE_URL, tlsAllowInvalidCertificates=True, ) db = client[MONGO_DATABASE] Request(scope).state.db = db - await self._app(scope, receive, send) + async with DB_SESSION_MAKER() as session, session.begin(): + Request(scope).state.psql_db = session + await self._app(scope, receive, send) class ProtectedDocsMiddleware: diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 336e28b..e69de29 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,19 +0,0 @@ -from .antispam import AntiSpam -from .discord_role import DiscordRole -from .discord_user import DiscordMember, DiscordUser -from .form import Form, FormList -from .form_response import FormResponse, ResponseList -from .question import CodeQuestion, Question - -__all__ = [ - "AntiSpam", - "CodeQuestion", - "DiscordMember", - "DiscordRole", - "DiscordUser", - "Form", - "FormList", - "FormResponse", - "Question", - "ResponseList", -] diff --git a/backend/models/dtos/__init__.py b/backend/models/dtos/__init__.py new file mode 100644 index 0000000..336e28b --- /dev/null +++ b/backend/models/dtos/__init__.py @@ -0,0 +1,19 @@ +from .antispam import AntiSpam +from .discord_role import DiscordRole +from .discord_user import DiscordMember, DiscordUser +from .form import Form, FormList +from .form_response import FormResponse, ResponseList +from .question import CodeQuestion, Question + +__all__ = [ + "AntiSpam", + "CodeQuestion", + "DiscordMember", + "DiscordRole", + "DiscordUser", + "Form", + "FormList", + "FormResponse", + "Question", + "ResponseList", +] diff --git a/backend/models/antispam.py b/backend/models/dtos/antispam.py similarity index 100% rename from backend/models/antispam.py rename to backend/models/dtos/antispam.py diff --git a/backend/models/discord_role.py b/backend/models/dtos/discord_role.py similarity index 100% rename from backend/models/discord_role.py rename to backend/models/dtos/discord_role.py diff --git a/backend/models/discord_user.py b/backend/models/dtos/discord_user.py similarity index 100% rename from backend/models/discord_user.py rename to backend/models/dtos/discord_user.py diff --git a/backend/models/form.py b/backend/models/dtos/form.py similarity index 100% rename from backend/models/form.py rename to backend/models/dtos/form.py diff --git a/backend/models/form_response.py b/backend/models/dtos/form_response.py similarity index 100% rename from backend/models/form_response.py rename to backend/models/dtos/form_response.py diff --git a/backend/models/question.py b/backend/models/dtos/question.py similarity index 100% rename from backend/models/question.py rename to backend/models/dtos/question.py diff --git a/backend/models/orm/__init__.py b/backend/models/orm/__init__.py new file mode 100644 index 0000000..4b4e99b --- /dev/null +++ b/backend/models/orm/__init__.py @@ -0,0 +1,39 @@ +"""Database models.""" + +from .admins import Admin +from .base import Base +from .form_questions import ( + FormCheckboxQuestion, + FormCodeQuestion, + FormCodeQuestionTest, + FormQuestion, + FormRadioQuestion, + FormRangeQuestion, + FormSectionQuestion, + FormSelectQuestion, + FormTextQuestion, + FormTimezoneQuestion, + FormVoteQuestion, +) +from .form_responses import FormResponse +from .forms import Form, FormEditor, FormFeatures + +__all__ = ( + "Admin", + "Base", + "Form", + "FormCheckboxQuestion", + "FormCodeQuestion", + "FormCodeQuestionTest", + "FormEditor", + "FormFeatures", + "FormQuestion", + "FormRadioQuestion", + "FormRangeQuestion", + "FormResponse", + "FormSectionQuestion", + "FormSelectQuestion", + "FormTextQuestion", + "FormTimezoneQuestion", + "FormVoteQuestion", +) diff --git a/backend/models/orm/admins.py b/backend/models/orm/admins.py new file mode 100644 index 0000000..7eee008 --- /dev/null +++ b/backend/models/orm/admins.py @@ -0,0 +1,14 @@ +"""Discord members who have admin access.""" + +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.types import BigInteger + +from .base import Base + + +class Admin(Base): + """A discord user_id that has admin level access to forms.""" + + __tablename__ = "admins" + + user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True) diff --git a/backend/models/orm/base.py b/backend/models/orm/base.py new file mode 100644 index 0000000..adf9270 --- /dev/null +++ b/backend/models/orm/base.py @@ -0,0 +1,25 @@ +"""The base classes for ORM models.""" + +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.schema import MetaData + +NAMING_CONVENTIONS = { + "ix": "%(column_0_label)s_ix", + "uq": "%(table_name)s_%(column_0_name)s_uq", + "ck": "%(table_name)s_%(constraint_name)s_ck", + "fk": "%(table_name)s_%(column_0_name)s_%(referred_table_name)s_fk", + "pk": "%(table_name)s_pk", +} + + +class Base(AsyncAttrs, DeclarativeBase): + """Classes that inherit this class will be automatically mapped using declarative mapping.""" + + metadata = MetaData(naming_convention=NAMING_CONVENTIONS) + + def patch_from_pydantic(self, pydantic_model: BaseModel) -> None: + """Patch this model using the given pydantic model, unspecified attributes remain the same.""" + for key, value in pydantic_model.dict(exclude_unset=True).items(): + setattr(self, key, value) diff --git a/backend/models/orm/form_questions.py b/backend/models/orm/form_questions.py new file mode 100644 index 0000000..cde1d0b --- /dev/null +++ b/backend/models/orm/form_questions.py @@ -0,0 +1,198 @@ +"""Discord members who have admin access.""" + +from typing import ClassVar + +from sqlalchemy import Enum, ForeignKey, Text +from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.constants import TextType + +from .base import Base + + +class FormQuestion(Base): + __tablename__ = "form_questions" + + question_id: Mapped[int] = mapped_column(primary_key=True) + form_id: Mapped[int] = mapped_column(ForeignKey("forms.form_id")) + name: Mapped[str] + type: Mapped[str] + required: Mapped[bool] + + __mapper_args__: ClassVar = {"polymorphic_identity": "form_questions", "polymorphic_on": "type"} + + def __repr__(self): + return f"{self.__class__.__name__}({self.name!r})" + + +class QuestionWithOptions: + options: Mapped[list[str]] = mapped_column( + ARRAY(Text), + nullable=False, + use_existing_column=True, + ) + + +class FormRadioQuestion(QuestionWithOptions, FormQuestion): + """A radio question type.""" + + __tablename__ = "form_radio_questions" + + radio_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_radio_questions", + } + + +class FormCheckboxQuestion(QuestionWithOptions, FormQuestion): + """A radio question type.""" + + __tablename__ = "form_checkbox_questions" + + checkbox_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_checkbox_questions", + } + + +class FormRangeQuestion(QuestionWithOptions, FormQuestion): + """A range question type.""" + + __tablename__ = "form_range_questions" + + range_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_range_questions", + } + + +class FormVoteQuestion(QuestionWithOptions, FormQuestion): + """A vote question type.""" + + __tablename__ = "form_vote_questions" + + range_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_vote_questions", + } + + +class FormSelectQuestion(QuestionWithOptions, FormQuestion): + """A select question type.""" + + __tablename__ = "form_select_questions" + + select_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_select_questions", + } + + +class FormTextQuestion(FormQuestion): + """A text question type.""" + + __tablename__ = "form_text_questions" + + text_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + text_type: Mapped[TextType] = mapped_column(Enum(TextType)) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_text_questions", + } + + +class FormCodeQuestion(FormQuestion): + """A code question type.""" + + __tablename__ = "form_code_questions" + + code_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + language: Mapped[str] + allow_failure: Mapped[bool] + unit_tests: Mapped[list["FormCodeQuestionTest"]] = relationship( + cascade="all, delete", + passive_deletes=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_code_questions", + } + + +class FormCodeQuestionTest(Base): + """Unit tests for a given code question.""" + + __tablename__ = "form_code_question_tests" + test_id: Mapped[int] = mapped_column(primary_key=True) + code_question_id: Mapped[int] = mapped_column( + ForeignKey("form_code_questions.code_question_id") + ) + name: Mapped[str] + code: Mapped[str] + + +class FormSectionQuestion(FormQuestion): + """A section question type.""" + + __tablename__ = "form_section_questions" + + section_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + text: Mapped[str] + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_section_questions", + } + + +class FormTimezoneQuestion(FormQuestion): + """A timezone question type.""" + + __tablename__ = "form_timezone_questions" + + timezone_question_id: Mapped[int] = mapped_column( + ForeignKey("form_questions.question_id"), + primary_key=True, + ) + + __mapper_args__: ClassVar = { + "polymorphic_load": "selectin", + "polymorphic_identity": "form_timezone_questions", + } diff --git a/backend/models/orm/form_responses.py b/backend/models/orm/form_responses.py new file mode 100644 index 0000000..d9876b9 --- /dev/null +++ b/backend/models/orm/form_responses.py @@ -0,0 +1,28 @@ +"""A submitted response to a form.""" + +from datetime import datetime + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.types import BigInteger, DateTime, Text + +from .base import Base + + +class FormResponse(Base): + """A submitted response to a form.""" + + __tablename__ = "form_responses" + + response_id: Mapped[int] = mapped_column(primary_key=True) + form_id: Mapped[int] = mapped_column(ForeignKey("forms.form_id")) + submitted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + + user_id: Mapped[int] = mapped_column(BigInteger) + username: Mapped[str] = mapped_column(Text) + user_email: Mapped[str] = mapped_column(Text, nullable=True) + user_is_admin: Mapped[bool] + + antispam_ip_hash: Mapped[str] = mapped_column(Text, nullable=True) + antispam_user_agent_hash: Mapped[str] = mapped_column(Text, nullable=True) + antispam_captcha_pass: Mapped[bool] = mapped_column(nullable=True) diff --git a/backend/models/orm/forms.py b/backend/models/orm/forms.py new file mode 100644 index 0000000..0c1a4e3 --- /dev/null +++ b/backend/models/orm/forms.py @@ -0,0 +1,75 @@ +"""All forms that can have submissions.""" + +from typing import TYPE_CHECKING + +import sqlalchemy.dialects.postgresql as pg +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.types import BigInteger, Enum, Text + +from backend.constants import FormFeatures + +from .base import Base + +if TYPE_CHECKING: + from . import FormQuestion, FormResponse + + +class Form(Base): + """A form that users can submit responses to.""" + + __tablename__ = "forms" + + form_id: Mapped[int] = mapped_column(primary_key=True) + + short_name: Mapped[str] = mapped_column(Text, nullable=False, index=True) + name: Mapped[str] = mapped_column(Text, nullable=False) + description: Mapped[str] = mapped_column(Text, nullable=False) + submission_text: Mapped[str | None] = mapped_column(Text, nullable=True) + + form_questions: Mapped[list["FormQuestion"]] = relationship( + cascade="all, delete", + passive_deletes=True, + ) + form_responses: Mapped[list["FormResponse"]] = relationship( + cascade="all, delete", + passive_deletes=True, + ) + + webhook_url: Mapped[str | None] = mapped_column(Text, nullable=True) + webhook_message: Mapped[str | None] = mapped_column(Text, nullable=True) + + discord_role: Mapped[int | None] = mapped_column(BigInteger, nullable=True) + + features: Mapped[list[FormFeatures]] = mapped_column(pg.ARRAY(Enum(FormFeatures), dimensions=1)) + + form_response_readers: Mapped[list["FormResponseReader"]] = relationship( + cascade="all, delete", + passive_deletes=True, + ) + form_editors: Mapped[list["FormEditor"]] = relationship( + cascade="all, delete", + passive_deletes=True, + ) + + +class FormResponseReader(Base): + """A Discord user that can read a given form.""" + + __tablename__ = "form_response_readers" + + form_id: Mapped[int] = mapped_column( + ForeignKey("forms.form_id", ondelete="CASCADE"), primary_key=True + ) + user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True) + + +class FormEditor(Base): + """A Discord user that can edit a given form.""" + + __tablename__ = "form_editors" + + form_id: Mapped[int] = mapped_column( + ForeignKey("forms.form_id", ondelete="CASCADE"), primary_key=True + ) + user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True) diff --git a/backend/routes/discord.py b/backend/routes/discord.py index 5cd6b47..1a56aac 100644 --- a/backend/routes/discord.py +++ b/backend/routes/discord.py @@ -6,7 +6,8 @@ from starlette.responses import JSONResponse from starlette.routing import Request -from backend import discord, models, route +from backend import discord, route +from backend.models import dtos from backend.validation import ErrorMessage, api NOT_FOUND_EXCEPTION = JSONResponse( @@ -24,7 +25,7 @@ class RolesRoute(route.Route): class RolesResponse(pydantic.BaseModel): """A list of all roles on the configured server.""" - roles: list[models.DiscordRole] + roles: list[dtos.DiscordRole] @requires(["authenticated", "admin"]) @api.validate( @@ -53,7 +54,7 @@ class MemberRequest(pydantic.BaseModel): @requires(["authenticated", "admin"]) @api.validate( - resp=Response(HTTP_200=models.DiscordMember, HTTP_400=ErrorMessage), + resp=Response(HTTP_200=dtos.DiscordMember, HTTP_400=ErrorMessage), json=MemberRequest, tags=["auth"], ) @@ -68,7 +69,7 @@ async def delete(self, request: Request) -> JSONResponse: @requires(["authenticated", "admin"]) @api.validate( - resp=Response(HTTP_200=models.DiscordMember, HTTP_400=ErrorMessage), + resp=Response(HTTP_200=dtos.DiscordMember, HTTP_400=ErrorMessage), json=MemberRequest, tags=["auth"], ) diff --git a/backend/routes/forms/condorcet.py b/backend/routes/forms/condorcet.py index 902770b..ac7e52e 100644 --- a/backend/routes/forms/condorcet.py +++ b/backend/routes/forms/condorcet.py @@ -9,7 +9,7 @@ from starlette.responses import JSONResponse from backend import discord -from backend.models import Form, FormResponse, Question +from backend.models.dtos import Form, FormResponse, Question from backend.route import Route from backend.validation import api diff --git a/backend/routes/forms/discover.py b/backend/routes/forms/discover.py index 0fe10b5..43e6cf3 100644 --- a/backend/routes/forms/discover.py +++ b/backend/routes/forms/discover.py @@ -5,7 +5,7 @@ from starlette.responses import JSONResponse from backend import constants -from backend.models import Form, FormList, Question +from backend.models.dtos import Form, FormList, Question from backend.route import Route from backend.validation import api diff --git a/backend/routes/forms/form.py b/backend/routes/forms/form.py index 86bbf49..c96d0d6 100644 --- a/backend/routes/forms/form.py +++ b/backend/routes/forms/form.py @@ -10,7 +10,7 @@ from starlette.responses import JSONResponse from backend import constants, discord -from backend.models import Form +from backend.models.dtos import Form from backend.route import Route from backend.routes.forms.discover import AUTH_FORM from backend.validation import ErrorMessage, OkayResponse, api diff --git a/backend/routes/forms/index.py b/backend/routes/forms/index.py index 1fdfc48..4b55af2 100644 --- a/backend/routes/forms/index.py +++ b/backend/routes/forms/index.py @@ -6,8 +6,8 @@ from starlette.responses import JSONResponse from backend.constants import WebHook -from backend.models import Form, FormList -from backend.models.form import validate_hook_url +from backend.models.dtos import Form, FormList +from backend.models.dtos.form import validate_hook_url from backend.route import Route from backend.validation import ErrorMessage, OkayResponse, api diff --git a/backend/routes/forms/response.py b/backend/routes/forms/response.py index b4f7f04..ac80b74 100644 --- a/backend/routes/forms/response.py +++ b/backend/routes/forms/response.py @@ -6,7 +6,7 @@ from starlette.responses import JSONResponse from backend import discord -from backend.models import FormResponse +from backend.models.dtos import FormResponse from backend.route import Route from backend.validation import ErrorMessage, OkayResponse, api diff --git a/backend/routes/forms/responses.py b/backend/routes/forms/responses.py index 85e5af2..4228af8 100644 --- a/backend/routes/forms/responses.py +++ b/backend/routes/forms/responses.py @@ -7,7 +7,7 @@ from starlette.responses import JSONResponse from backend import discord -from backend.models import FormResponse, ResponseList +from backend.models.dtos import FormResponse, ResponseList from backend.route import Route from backend.validation import ErrorMessage, OkayResponse, api diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index 01c32cc..45636b7 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -19,7 +19,7 @@ from backend import constants from backend.authentication.user import User -from backend.models import Form, FormResponse +from backend.models.dtos import Form, FormResponse from backend.route import Route from backend.routes.auth.authorize import set_response_token from backend.routes.forms.discover import AUTH_FORM diff --git a/backend/routes/forms/unittesting.py b/backend/routes/forms/unittesting.py index 57c3a86..469243c 100644 --- a/backend/routes/forms/unittesting.py +++ b/backend/routes/forms/unittesting.py @@ -8,7 +8,7 @@ from httpx import HTTPStatusError from backend.constants import SNEKBOX_URL -from backend.models import Form, FormResponse +from backend.models.dtos import Form, FormResponse with Path("resources/unittest_template.py").open(encoding="utf8") as file: TEST_TEMPLATE = file.read() diff --git a/docker-compose.yml b/docker-compose.yml index a9363f8..5be7843 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,22 +20,42 @@ services: ports: - "127.0.0.1:6379:6379" + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: backend + POSTGRES_PASSWORD: backend + POSTGRES_USER: backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U backend"] + interval: 2s + timeout: 1s + retries: 5 + ports: + - 5000:5432 + backend: build: . - command: ["uvicorn", "--reload", "--host", "0.0.0.0", "backend:app"] + command: ["poetry run alembic upgrade head && poetry run uvicorn backend:app --reload --host 0.0.0.0 --port 8000"] ports: - "127.0.0.1:8000:8000" depends_on: - - mongo - - snekbox - - redis + mongo: + condition: service_started + snekbox: + condition: service_started + redis: + condition: service_started + postgres: + condition: service_healthy tty: true env_file: - .env volumes: - .:/app:ro environment: - - DATABASE_URL=mongodb://forms-backend:forms-backend@mongo:27017 + - MONGO_DATABASE_URL=mongodb://forms-backend:forms-backend@mongo:27017 + - PSQL_DATABASE_URL=postgresql+psycopg_async://backend:backend@postgres:5432/backend - SNEKBOX_URL=http://snekbox:8060/eval - OAUTH2_CLIENT_ID - OAUTH2_CLIENT_SECRET diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..36ddea1 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,86 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool +from sqlalchemy.ext.asyncio import AsyncEngine +from sqlalchemy.ext.asyncio.engine import AsyncConnection + +# This is a required step by Alembic to properly generate migrations +import backend.models.orm +from backend.constants import PSQL_DATABASE_URL + +target_metadata = backend.models.orm.base.Base.metadata + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +config.set_main_option("sqlalchemy.url", PSQL_DATABASE_URL) + + +def run_migrations_offline() -> None: + """ + Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: AsyncConnection) -> None: + """Run all migrations on the given connection.""" + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online() -> None: + """ + Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = AsyncEngine( + engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + future=True, + ), + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..1d3e1aa --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,27 @@ +""" +${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +import sqlalchemy as sa +from alembic import op + +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + """Apply this migration.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Revert this migration.""" + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/1721486482-a9ea4b71d23a_add_admins_forms.py b/migrations/versions/1721486482-a9ea4b71d23a_add_admins_forms.py new file mode 100644 index 0000000..c61944b --- /dev/null +++ b/migrations/versions/1721486482-a9ea4b71d23a_add_admins_forms.py @@ -0,0 +1,93 @@ +""" +Add admins & forms. + +Revision ID: a9ea4b71d23a +Revises: +Create Date: 2024-07-20 14:41:22.166383+00:00 +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "a9ea4b71d23a" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Apply this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "admins", + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint("user_id", name=op.f("admins_pk")), + ) + op.create_table( + "forms", + sa.Column("form_id", sa.Integer(), nullable=False), + sa.Column("short_name", sa.Text(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("submission_text", sa.Text(), nullable=True), + sa.Column("webhook_url", sa.Text(), nullable=True), + sa.Column("webhook_message", sa.Text(), nullable=True), + sa.Column("discord_role", sa.BigInteger(), nullable=True), + sa.Column( + "features", + postgresql.ARRAY( + sa.Enum( + "DISCOVERABLE", + "REQUIRES_LOGIN", + "OPEN", + "COLLECT_EMAIL", + "DISABLE_ANTISPAM", + "WEBHOOK_ENABLED", + "ASSIGN_ROLE", + name="formfeatures", + ), + dimensions=1, + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("form_id", name=op.f("forms_pk")), + ) + op.create_index(op.f("forms_short_name_ix"), "forms", ["short_name"], unique=False) + op.create_table( + "form_editors", + sa.Column("form_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["form_id"], + ["forms.form_id"], + name=op.f("form_editors_form_id_forms_fk"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("form_id", "user_id", name=op.f("form_editors_pk")), + ) + op.create_table( + "form_response_readers", + sa.Column("form_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ["form_id"], + ["forms.form_id"], + name=op.f("form_response_readers_form_id_forms_fk"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("form_id", "user_id", name=op.f("form_response_readers_pk")), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Revert this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("form_response_readers") + op.drop_table("form_editors") + op.drop_index(op.f("forms_short_name_ix"), table_name="forms") + op.drop_table("forms") + op.drop_table("admins") + # ### end Alembic commands ### diff --git a/migrations/versions/1721493967-787116206e6d_add_form_question_models.py b/migrations/versions/1721493967-787116206e6d_add_form_question_models.py new file mode 100644 index 0000000..17acd61 --- /dev/null +++ b/migrations/versions/1721493967-787116206e6d_add_form_question_models.py @@ -0,0 +1,160 @@ +""" +Add form question models. + +Revision ID: 787116206e6d +Revises: a9ea4b71d23a +Create Date: 2024-07-20 16:46:07.590445+00:00 +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "787116206e6d" +down_revision = "a9ea4b71d23a" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Apply this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "form_questions", + sa.Column("question_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("type", sa.String(), nullable=False), + sa.Column("required", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("question_id", name=op.f("form_questions_pk")), + ) + op.create_table( + "form_checkbox_questions", + sa.Column("checkbox_question_id", sa.Integer(), nullable=False), + sa.Column("options", postgresql.ARRAY(sa.Text()), nullable=False), + sa.ForeignKeyConstraint( + ["checkbox_question_id"], + ["form_questions.question_id"], + name=op.f("form_checkbox_questions_checkbox_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("checkbox_question_id", name=op.f("form_checkbox_questions_pk")), + ) + op.create_table( + "form_code_questions", + sa.Column("code_question_id", sa.Integer(), nullable=False), + sa.Column("language", sa.String(), nullable=False), + sa.Column("allow_failure", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ["code_question_id"], + ["form_questions.question_id"], + name=op.f("form_code_questions_code_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("code_question_id", name=op.f("form_code_questions_pk")), + ) + op.create_table( + "form_radio_questions", + sa.Column("radio_question_id", sa.Integer(), nullable=False), + sa.Column("options", postgresql.ARRAY(sa.Text()), nullable=False), + sa.ForeignKeyConstraint( + ["radio_question_id"], + ["form_questions.question_id"], + name=op.f("form_radio_questions_radio_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("radio_question_id", name=op.f("form_radio_questions_pk")), + ) + op.create_table( + "form_range_questions", + sa.Column("range_question_id", sa.Integer(), nullable=False), + sa.Column("options", postgresql.ARRAY(sa.Text()), nullable=False), + sa.ForeignKeyConstraint( + ["range_question_id"], + ["form_questions.question_id"], + name=op.f("form_range_questions_range_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("range_question_id", name=op.f("form_range_questions_pk")), + ) + op.create_table( + "form_section_questions", + sa.Column("section_question_id", sa.Integer(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["section_question_id"], + ["form_questions.question_id"], + name=op.f("form_section_questions_section_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("section_question_id", name=op.f("form_section_questions_pk")), + ) + op.create_table( + "form_select_questions", + sa.Column("select_question_id", sa.Integer(), nullable=False), + sa.Column("options", postgresql.ARRAY(sa.Text()), nullable=False), + sa.ForeignKeyConstraint( + ["select_question_id"], + ["form_questions.question_id"], + name=op.f("form_select_questions_select_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("select_question_id", name=op.f("form_select_questions_pk")), + ) + op.create_table( + "form_text_questions", + sa.Column("text_question_id", sa.Integer(), nullable=False), + sa.Column("text_type", sa.Enum("SHORT_TEXT", "TEXT_AREA", name="texttype"), nullable=False), + sa.ForeignKeyConstraint( + ["text_question_id"], + ["form_questions.question_id"], + name=op.f("form_text_questions_text_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("text_question_id", name=op.f("form_text_questions_pk")), + ) + op.create_table( + "form_timezone_questions", + sa.Column("timezone_question_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["timezone_question_id"], + ["form_questions.question_id"], + name=op.f("form_timezone_questions_timezone_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("timezone_question_id", name=op.f("form_timezone_questions_pk")), + ) + op.create_table( + "form_vote_questions", + sa.Column("range_question_id", sa.Integer(), nullable=False), + sa.Column("options", postgresql.ARRAY(sa.Text()), nullable=False), + sa.ForeignKeyConstraint( + ["range_question_id"], + ["form_questions.question_id"], + name=op.f("form_vote_questions_range_question_id_form_questions_fk"), + ), + sa.PrimaryKeyConstraint("range_question_id", name=op.f("form_vote_questions_pk")), + ) + op.create_table( + "form_code_question_tests", + sa.Column("test_id", sa.Integer(), nullable=False), + sa.Column("code_question_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("code", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["code_question_id"], + ["form_code_questions.code_question_id"], + name=op.f("form_code_question_tests_code_question_id_form_code_questions_fk"), + ), + sa.PrimaryKeyConstraint("test_id", name=op.f("form_code_question_tests_pk")), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Revert this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("form_code_question_tests") + op.drop_table("form_vote_questions") + op.drop_table("form_timezone_questions") + op.drop_table("form_text_questions") + op.drop_table("form_select_questions") + op.drop_table("form_section_questions") + op.drop_table("form_range_questions") + op.drop_table("form_radio_questions") + op.drop_table("form_code_questions") + op.drop_table("form_checkbox_questions") + op.drop_table("form_questions") + # ### end Alembic commands ### diff --git a/migrations/versions/1721564095-bef2f206168e_link_form_questions_to_forms.py b/migrations/versions/1721564095-bef2f206168e_link_form_questions_to_forms.py new file mode 100644 index 0000000..e84ecf4 --- /dev/null +++ b/migrations/versions/1721564095-bef2f206168e_link_form_questions_to_forms.py @@ -0,0 +1,36 @@ +""" +Link form questions to forms. + +Revision ID: bef2f206168e +Revises: 787116206e6d +Create Date: 2024-07-21 12:14:55.545648+00:00 +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "bef2f206168e" +down_revision = "787116206e6d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Apply this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("form_questions", sa.Column("form_id", sa.Integer(), nullable=False)) + op.create_foreign_key( + op.f("form_questions_form_id_forms_fk"), "form_questions", "forms", ["form_id"], ["form_id"] + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Revert this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + op.f("form_questions_form_id_forms_fk"), "form_questions", type_="foreignkey" + ) + op.drop_column("form_questions", "form_id") + # ### end Alembic commands ### diff --git a/migrations/versions/1721565620-9ee599d0f323_add_form_responses.py b/migrations/versions/1721565620-9ee599d0f323_add_form_responses.py new file mode 100644 index 0000000..1927f16 --- /dev/null +++ b/migrations/versions/1721565620-9ee599d0f323_add_form_responses.py @@ -0,0 +1,46 @@ +""" +Add form responses. + +Revision ID: 9ee599d0f323 +Revises: bef2f206168e +Create Date: 2024-07-21 12:40:20.326205+00:00 +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9ee599d0f323" +down_revision = "bef2f206168e" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Apply this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "form_responses", + sa.Column("response_id", sa.Integer(), nullable=False), + sa.Column("form_id", sa.Integer(), nullable=False), + sa.Column("submitted_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("username", sa.Text(), nullable=False), + sa.Column("user_email", sa.Text(), nullable=True), + sa.Column("user_is_admin", sa.Boolean(), nullable=False), + sa.Column("antispam_ip_hash", sa.Text(), nullable=True), + sa.Column("antispam_user_agent_hash", sa.Text(), nullable=True), + sa.Column("antispam_captcha_pass", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["form_id"], ["forms.form_id"], name=op.f("form_responses_form_id_forms_fk") + ), + sa.PrimaryKeyConstraint("response_id", name=op.f("form_responses_pk")), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Revert this migration.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("form_responses") + # ### end Alembic commands ### diff --git a/migrations/versions/__init__.py b/migrations/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock index 7582cd4..09553fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "alembic" +version = "1.13.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + [[package]] name = "anyio" version = "4.4.0" @@ -147,43 +166,38 @@ pytest-mock = ">=3.6.1,<4.0.0" [[package]] name = "cryptography" -version = "42.0.8" +version = "43.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, - {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, - {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, - {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, - {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, - {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, - {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, ] [package.dependencies] @@ -196,7 +210,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -257,6 +271,77 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.14.0" @@ -397,6 +482,94 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mako" +version = "1.3.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "motor" version = "3.5.1" @@ -502,6 +675,92 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "psycopg" +version = "3.2.1" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +typing-extensions = ">=4.4" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.1)"] +c = ["psycopg-c (==3.2.1)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.1" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -663,20 +922,20 @@ zstd = ["zstandard"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, + {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -789,29 +1048,29 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "ruff" -version = "0.5.3" +version = "0.5.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.3-py3-none-linux_armv6l.whl", hash = "sha256:b12424d9db7347fa63c5ed9af010003338c63c629fb9c9c6adb2aa4f5699729b"}, - {file = "ruff-0.5.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8d72c5684bbd4ed304a9a955ee2e67f57b35f6193222ade910cca8a805490e3"}, - {file = "ruff-0.5.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d2fc2cdb85ccac1e816cc9d5d8cedefd93661bd957756d902543af32a6b04a71"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf4bc751240b2fab5d19254571bcacb315c7b0b00bf3c912d52226a82bbec073"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc697ec874fdd7c7ba0a85ec76ab38f8595224868d67f097c5ffc21136e72fcd"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e791d34d3557a3819b3704bc1f087293c821083fa206812842fa363f6018a192"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:76bb5a87fd397520b91a83eae8a2f7985236d42dd9459f09eef58e7f5c1d8316"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8cfc7a26422c78e94f1ec78ec02501bbad2df5834907e75afe474cc6b83a8c1"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96066c4328a49fce2dd40e80f7117987369feec30ab771516cf95f1cc2db923c"}, - {file = "ruff-0.5.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfe9ab5bdc0b08470c3b261643ad54ea86edc32b64d1e080892d7953add3ad"}, - {file = "ruff-0.5.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7704582a026fa02cca83efd76671a98ee6eb412c4230209efe5e2a006c06db62"}, - {file = "ruff-0.5.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:08058d077e21b856d32ebf483443390e29dc44d927608dc8f092ff6776519da9"}, - {file = "ruff-0.5.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77d49484429ed7c7e6e2e75a753f153b7b58f875bdb4158ad85af166a1ec1822"}, - {file = "ruff-0.5.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:642cbff6cbfa38d2566d8db086508d6f472edb136cbfcc4ea65997745368c29e"}, - {file = "ruff-0.5.3-py3-none-win32.whl", hash = "sha256:eafc45dd8bdc37a00b28e68cc038daf3ca8c233d73fea276dcd09defb1352841"}, - {file = "ruff-0.5.3-py3-none-win_amd64.whl", hash = "sha256:cbaec2ddf4f78e5e9ecf5456ea0f496991358a1d883862ed0b9e947e2b6aea93"}, - {file = "ruff-0.5.3-py3-none-win_arm64.whl", hash = "sha256:05fbd2cb404775d6cd7f2ff49504e2d20e13ef95fa203bd1ab22413af70d420b"}, - {file = "ruff-0.5.3.tar.gz", hash = "sha256:2a3eb4f1841771fa5b67a56be9c2d16fd3cc88e378bd86aaeaec2f7e6bcdd0a2"}, + {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, + {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, + {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, + {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, + {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, + {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, + {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, + {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, + {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, ] [[package]] @@ -898,6 +1157,93 @@ flask = ["flask"] quart = ["quart"] starlette = ["starlette[full]"] +[[package]] +name = "sqlalchemy" +version = "2.0.31" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, + {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, + {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.38.0" @@ -981,6 +1327,17 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + [[package]] name = "urllib3" version = "2.2.2" @@ -1258,4 +1615,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "87d3616ee3731f4a180dfb8f8acc630e94b25d9c486311c90f35e9d8622d1c5e" +content-hash = "2aa841718abdad1aad43b31843f4cb3cc292c75ac7e23db80b84d41407361cf0" diff --git a/pyproject.toml b/pyproject.toml index 1bf7240..c5a18b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,22 +4,29 @@ version = "0.1.0" description = "Backend for Python Discord Forms" authors = ["Joe Banks "] license = "MIT" +package-mode = false [tool.poetry.dependencies] python = "^3.12" + starlette = "^0.38.0" nested_dict = "^1.61" uvicorn = { extras = ["standard"], version = "^0.30.1" } -motor = "3.5.1" +httpx = "^0.27.0" + python-dotenv = "^1.0.1" pyjwt = "^2.8.0" -httpx = "^0.27.0" pydantic = "^1.10.17" spectree = "^1.2.10" deepmerge = "^1.1.1" sentry-sdk = "^2.7.1" condorcet = "^0.1.1" + redis = "^5.0.7" +motor = "3.5.1" +alembic = {version = "^1.13.2", extras = ["tz"]} +sqlalchemy = {version = "^2.0.31", extras = ["asyncio"]} +psycopg = {version = "3.2.1", extras = ["binary"]} [tool.poetry.group.typing.dependencies] types-redis = "^4.6.0.20240425" @@ -71,6 +78,10 @@ ignore = [ ] [tool.ruff.lint.isort] +known-first-party = ["migrations", "backend"] order-by-type = false case-sensitive = true combine-as-imports = true + +[tool.ruff.lint.per-file-ignores] +"migrations/*" = ["N999"]