diff --git a/Dockerfile b/Dockerfile index 88bbe550..adc16291 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ WORKDIR /usr/src/poetry # Install dependencies with Poetry RUN poetry lock -RUN poetry install --no-root --only main +RUN poetry install --no-root # Selecting a working directory WORKDIR /usr/src/fastapi diff --git a/docker-compose-tests.yml b/docker-compose-tests.yml index bae63e8a..8c5000a1 100644 --- a/docker-compose-tests.yml +++ b/docker-compose-tests.yml @@ -4,11 +4,15 @@ services: context: . dockerfile: ./docker/tests/Dockerfile container_name: backend_theater_test - command: [ "pytest", "-c", "/usr/src/config/pytest.ini", - "-m", "e2e", "--maxfail=5", "--disable-warnings", "-v", "--tb=short"] + command: > + sh -c " + alembic upgrade head && + pytest -c /usr/src/config/pytest.ini -m e2e --maxfail=5 --disable-warnings -v --tb=short + " environment: - PYTHONPATH=/usr/src/fastapi - ENVIRONMENT=testing + - ALEMBIC_CONFIG=/usr/src/config/alembic.ini - EMAIL_HOST=mailhog_theater_test - EMAIL_PORT=1025 - EMAIL_HOST_USER=testuser@mate.com @@ -27,6 +31,7 @@ services: condition: service_healthy volumes: - ./src:/usr/src/fastapi + - ./alembic.ini:/usr/src/config/alembic.ini networks: - theater_network_test diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile index a6a0c3b8..82c24230 100644 --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -4,9 +4,16 @@ FROM python:3.10-slim ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV PIP_NO_CACHE_DIR=off +ENV ALEMBIC_CONFIG=/usr/src/config/alembic.ini # Installing dependencies -RUN apt update +RUN apt update && apt install -y \ + gcc \ + libpq-dev \ + netcat-openbsd \ + postgresql-client \ + dos2unix \ + && apt clean # Install Poetry RUN python -m pip install --upgrade pip && \ @@ -27,10 +34,18 @@ WORKDIR /usr/src/poetry # Install dependencies with Poetry RUN poetry lock -RUN poetry install --no-root --only main +RUN poetry install --no-root # Selecting a working directory WORKDIR /usr/src/fastapi # Copy the source code COPY ./src . + +COPY ./alembic.ini /usr/src/config/alembic.ini + +COPY ./commands /commands +RUN dos2unix /commands/*.sh && chmod +x /commands/*.sh + +CMD ["pytest", "-c", "/usr/src/config/pytest.ini", "-m", "e2e", "--maxfail=5", "--disable-warnings", "-v", "--tb=short"] + diff --git a/poetry.lock b/poetry.lock index 9ff15f88..c2cc5e72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "aioboto3" @@ -7,7 +7,6 @@ description = "Async boto3 wrapper" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aioboto3-13.4.0-py3-none-any.whl", hash = "sha256:d78f3400ef3a01b4d5515108ef244941894a0bc39c4716321a00e15898d7e002"}, {file = "aioboto3-13.4.0.tar.gz", hash = "sha256:3105f9e5618c686c90050e60eb5ebf9e28f7f8c4e0fa162d4481aaa402008aab"}, @@ -28,7 +27,6 @@ description = "Async client for aws services using botocore and aiohttp" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiobotocore-2.18.0-py3-none-any.whl", hash = "sha256:89634470946944baf0a72fe2939cdd5f98b61335d400ca55f3032aca92989ec1"}, {file = "aiobotocore-2.18.0.tar.gz", hash = "sha256:c54db752c5a742bf1a05c8359a93f508b4bf702b0e6be253a4c9ef1f9c9b6706"}, @@ -56,7 +54,6 @@ description = "File support for asyncio." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, @@ -69,7 +66,6 @@ description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"}, {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"}, @@ -82,7 +78,6 @@ description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"}, {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"}, @@ -178,7 +173,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aioitertools" @@ -187,7 +182,6 @@ description = "itertools and builtins for AsyncIO and mixed iterables" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796"}, {file = "aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b"}, @@ -204,7 +198,6 @@ description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, @@ -220,7 +213,6 @@ description = "asyncio SMTP client" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiosmtplib-4.0.0-py3-none-any.whl", hash = "sha256:33c72021cd9e9da495823952751e2dd927014a04a0eca711ee4af9812f2f04af"}, {file = "aiosmtplib-4.0.0.tar.gz", hash = "sha256:9629a0d8786ab1e5f790ebbbf5cbe7886fedf949a3f52fd7b27a0360f6233422"}, @@ -237,7 +229,6 @@ description = "asyncio bridge to the standard sqlite3 module" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, @@ -257,7 +248,6 @@ description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, @@ -269,7 +259,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["backports.zoneinfo"] +tz = ["backports.zoneinfo ; python_version < \"3.9\""] [[package]] name = "annotated-types" @@ -278,7 +268,6 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -291,7 +280,6 @@ description = "High level compatibility layer for multiple asynchronous event lo optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, @@ -305,7 +293,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -315,7 +303,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version < \"3.11.0\"" +markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -328,7 +316,6 @@ description = "An asyncio PostgreSQL driver" optional = false python-versions = ">=3.8.0" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -386,8 +373,8 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi", "sspilib"] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] [[package]] name = "attrs" @@ -396,19 +383,18 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "bcrypt" @@ -417,7 +403,6 @@ description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.6" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, @@ -453,7 +438,6 @@ description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -476,7 +460,6 @@ description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "boto3-1.36.1-py3-none-any.whl", hash = "sha256:eb21380d73fec6645439c0d802210f72a0cdb3295b02953f246ff53f512faa8f"}, {file = "boto3-1.36.1.tar.gz", hash = "sha256:258ab77225a81d3cf3029c9afe9920cd9dec317689dfadec6f6f0a23130bb60a"}, @@ -497,7 +480,6 @@ description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "botocore-1.36.1-py3-none-any.whl", hash = "sha256:dec513b4eb8a847d79bbefdcdd07040ed9d44c20b0001136f0890a03d595705a"}, {file = "botocore-1.36.1.tar.gz", hash = "sha256:f789a6f272b5b3d8f8756495019785e33868e5e00dd9662a3ee7959ac939bb12"}, @@ -518,7 +500,6 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -531,7 +512,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.12\" and platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -612,7 +593,6 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -628,7 +608,7 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] -markers = "python_version <= \"3.11\" and sys_platform == \"win32\" or python_version <= \"3.11\" and platform_system == \"Windows\" or python_version >= \"3.12\" and sys_platform == \"win32\" or python_version >= \"3.12\" and platform_system == \"Windows\"" +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -641,7 +621,6 @@ description = "cryptography is a package which provides cryptographic recipes an optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, @@ -649,7 +628,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -660,7 +638,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -678,10 +655,10 @@ files = [ cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -694,7 +671,6 @@ description = "DNS toolkit" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, @@ -716,7 +692,6 @@ description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, @@ -736,7 +711,6 @@ description = "A robust email address syntax and deliverability validation libra optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, @@ -753,7 +727,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -769,7 +743,6 @@ description = "FastAPI framework, high performance, easy to learn, fast to code, optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, @@ -791,7 +764,6 @@ description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, @@ -809,7 +781,6 @@ description = "A list-like structure which implements collections.abc.MutableSeq optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -912,7 +883,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" 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 python_version >= \"3.12\" and 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\")" +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\")" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -1000,7 +971,6 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -1013,7 +983,6 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -1036,7 +1005,6 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1049,7 +1017,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1062,7 +1030,6 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1078,7 +1045,6 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -1091,7 +1057,6 @@ description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -1110,7 +1075,6 @@ description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -1123,7 +1087,6 @@ description = "A super-fast templating language that borrows the best ideas from optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, @@ -1144,7 +1107,6 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1216,7 +1178,6 @@ description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1229,7 +1190,6 @@ description = "multidict implementation" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -1335,7 +1295,6 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, @@ -1401,7 +1360,6 @@ description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1414,7 +1372,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -1502,7 +1459,6 @@ description = "comprehensive password hashing framework supporting over 30 schem optional = false python-versions = "*" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, @@ -1524,7 +1480,6 @@ description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, @@ -1604,7 +1559,7 @@ docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -1614,7 +1569,6 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1631,7 +1585,6 @@ description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, @@ -1724,7 +1677,6 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -1773,6 +1725,7 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -1802,7 +1755,6 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -1815,7 +1767,6 @@ description = "Python style guide checker" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, @@ -1828,7 +1779,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.12\" and platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1841,7 +1792,6 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, @@ -1854,7 +1804,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1863,7 +1813,6 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -1977,7 +1926,6 @@ description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"}, {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"}, @@ -1999,12 +1947,26 @@ description = "passive checker of Python programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.3.4" @@ -2012,7 +1974,6 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -2036,7 +1997,6 @@ description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, @@ -2056,7 +2016,6 @@ description = "pytest plugin that allows you to add environment variables." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30"}, {file = "pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf"}, @@ -2076,7 +2035,6 @@ description = "pytest plugin to run your tests in a specific order" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_order-1.3.0-py3-none-any.whl", hash = "sha256:2cd562a21380345dd8d5774aa5fd38b7849b6ee7397ca5f6999bbe6e89f07f6e"}, {file = "pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde"}, @@ -2092,7 +2050,6 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2108,7 +2065,6 @@ description = "Read key-value pairs from a .env file and set them as environment optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -2124,7 +2080,6 @@ description = "JOSE implementation in Python" optional = false python-versions = "*" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, @@ -2148,7 +2103,6 @@ description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, @@ -2161,7 +2115,6 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -2174,7 +2127,6 @@ description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -2190,17 +2142,16 @@ description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"}, {file = "s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f"}, ] [package.dependencies] -botocore = ">=1.36.0,<2.0a.0" +botocore = ">=1.36.0,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] +crt = ["botocore[crt] (>=1.36.0,<2.0a0)"] [[package]] name = "six" @@ -2209,7 +2160,6 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2222,7 +2172,6 @@ description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2235,7 +2184,6 @@ description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -2248,7 +2196,6 @@ description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, @@ -2345,7 +2292,6 @@ description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, @@ -2364,7 +2310,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2407,7 +2353,6 @@ description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -2430,7 +2375,6 @@ description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2443,7 +2387,6 @@ description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -2456,14 +2399,13 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2475,7 +2417,6 @@ description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -2487,7 +2428,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "validators" @@ -2496,7 +2437,6 @@ description = "Python Data Validation for Humans™" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321"}, {file = "validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f"}, @@ -2512,7 +2452,6 @@ description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, @@ -2602,7 +2541,6 @@ description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, @@ -2696,4 +2634,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "6364614af5e65849c726f86aca146e18e822d35ff0836eab1979f98435e3d6d5" +content-hash = "bca174dfb170778fa5045241367bdebc2877c91e322a3686e3597e125a814a4e" diff --git a/pyproject.toml b/pyproject.toml index e579412a..1f176b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ asyncpg = "^0.30.0" aiosqlite = "^0.21.0" aioboto3 = "^13.4.0" pytest-asyncio = "^0.25.3" - +pygments = "*" [build-system] requires = ["poetry-core"] diff --git a/src/config/dependencies.py b/src/config/dependencies.py index de3e2b3a..2781ae76 100644 --- a/src/config/dependencies.py +++ b/src/config/dependencies.py @@ -103,3 +103,4 @@ def get_s3_storage_client( secret_key=settings.S3_STORAGE_SECRET_KEY, bucket_name=settings.S3_BUCKET_NAME ) + diff --git a/src/config/settings.py b/src/config/settings.py index f05b1df4..b83af670 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -47,12 +47,27 @@ class Settings(BaseAppSettings): SECRET_KEY_REFRESH: str = os.getenv("SECRET_KEY_REFRESH", os.urandom(32)) JWT_SIGNING_ALGORITHM: str = os.getenv("JWT_SIGNING_ALGORITHM", "HS256") + @property + def DATABASE_URL(self) -> str: + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" + f"@{self.POSTGRES_HOST}:{self.POSTGRES_DB_PORT}" + ) class TestingSettings(BaseAppSettings): SECRET_KEY_ACCESS: str = "SECRET_KEY_ACCESS" SECRET_KEY_REFRESH: str = "SECRET_KEY_REFRESH" JWT_SIGNING_ALGORITHM: str = "HS256" + DATABASE_URL: str = "sqlite+aiosqlite:///./test.db" + + EMAIL_HOST: str = "localhost" + EMAIL_PORT: int = 1025 + EMAIL_HOST_USER: str = "testuser@mate.com" + EMAIL_HOST_PASSWORD: str = "test_password" + EMAIL_USE_TLS: bool = False + MAILHOG_API_PORT: int = 8025 + def model_post_init(self, __context: dict[str, Any] | None = None) -> None: object.__setattr__(self, 'PATH_TO_DB', ":memory:") object.__setattr__( @@ -60,3 +75,9 @@ def model_post_init(self, __context: dict[str, Any] | None = None) -> None: 'PATH_TO_MOVIES_CSV', str(self.BASE_DIR / "database" / "seed_data" / "test_data.csv") ) + +environment = os.getenv('ENVIRONMENT', 'developing') +if environment == 'testing': + settings = TestingSettings() +else: + settings = Settings() diff --git a/src/database/migrations/env.py b/src/database/migrations/env.py index d31aafca..a725b74e 100644 --- a/src/database/migrations/env.py +++ b/src/database/migrations/env.py @@ -1,10 +1,11 @@ from logging.config import fileConfig from alembic import context +from sqlalchemy import create_engine +from config import get_settings from database.models import movies, accounts # noqa: F401 from database.models.base import Base -from database.session_postgresql import sync_postgresql_engine # this is the Alembic Config object, which provides @@ -26,7 +27,10 @@ # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. +settings = get_settings() +DATABASE_URL = settings.DATABASE_URL.replace("+aiosqlite", "") +connectable = create_engine(DATABASE_URL) def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -40,18 +44,17 @@ def run_migrations_offline() -> None: script output. """ - connectable = sync_postgresql_engine - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=True, - compare_server_default=True - ) + context.configure( + url=DATABASE_URL, + target_metadata=target_metadata, + literal_binds=True, + compare_type=True, + compare_server_default=True + ) - with context.begin_transaction(): - context.run_migrations() + with context.begin_transaction(): + context.run_migrations() def run_migrations_online() -> None: @@ -61,7 +64,6 @@ def run_migrations_online() -> None: and associate a connection with the context. """ - connectable = sync_postgresql_engine with connectable.connect() as connection: context.configure( diff --git a/src/database/migrations/versions/2da0dc469be8_temp_migration.py b/src/database/migrations/versions/2da0dc469be8_temp_migration.py index 9944cb30..04319004 100644 --- a/src/database/migrations/versions/2da0dc469be8_temp_migration.py +++ b/src/database/migrations/versions/2da0dc469be8_temp_migration.py @@ -19,6 +19,10 @@ def upgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == 'sqlite': + return + # ### commands auto generated by Alembic - please adjust! ### op.alter_column('activation_tokens', 'token', existing_type=sa.VARCHAR(length=255), diff --git a/src/database/migrations/versions/32b1054a69e3_initial_migration.py b/src/database/migrations/versions/32b1054a69e3_initial_migration.py index b1a7e13b..7feff14f 100644 --- a/src/database/migrations/versions/32b1054a69e3_initial_migration.py +++ b/src/database/migrations/versions/32b1054a69e3_initial_migration.py @@ -13,12 +13,20 @@ # revision identifiers, used by Alembic. revision: str = '32b1054a69e3' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = None +branch_labels: str | None = None +depends_on: str | None = None def upgrade() -> None: + bind = op.get_bind() + dialect = bind.dialect.name + + if dialect == 'sqlite': + default_timestamp = sa.text('CURRENT_TIMESTAMP') + else: + default_timestamp = sa.func.now() + # ### commands auto generated by Alembic - please adjust! ### op.create_table('actors', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), @@ -70,8 +78,8 @@ def upgrade() -> None: sa.Column('email', sa.String(length=255), nullable=False), sa.Column('hashed_password', sa.String(length=255), nullable=False), sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=default_timestamp, nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=default_timestamp, nullable=False), sa.Column('group_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['group_id'], ['user_groups.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') diff --git a/src/database/migrations/versions/41cdafa531cf_temp_migration.py b/src/database/migrations/versions/41cdafa531cf_temp_migration.py index f205e883..93416b12 100644 --- a/src/database/migrations/versions/41cdafa531cf_temp_migration.py +++ b/src/database/migrations/versions/41cdafa531cf_temp_migration.py @@ -19,6 +19,10 @@ def upgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == 'sqlite': + return + # ### commands auto generated by Alembic - please adjust! ### op.alter_column('refresh_tokens', 'token', existing_type=sa.VARCHAR(length=64), @@ -28,6 +32,10 @@ def upgrade() -> None: def downgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == 'sqlite': + return + # ### commands auto generated by Alembic - please adjust! ### op.alter_column('refresh_tokens', 'token', existing_type=sa.String(length=512), diff --git a/src/routes/accounts.py b/src/routes/accounts.py index 82729aac..db449d73 100644 --- a/src/routes/accounts.py +++ b/src/routes/accounts.py @@ -1,7 +1,8 @@ from datetime import datetime, timezone from typing import cast -from fastapi import APIRouter, Depends, status, HTTPException +from fastapi import APIRouter, Depends, status, HTTPException, BackgroundTasks + from sqlalchemy import select, delete from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession @@ -67,7 +68,9 @@ ) async def register_user( user_data: UserRegistrationRequestSchema, + background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator) ) -> UserRegistrationResponseSchema: """ Endpoint for user registration. @@ -101,10 +104,10 @@ async def register_user( result = await db.execute(stmt) user_group = result.scalars().first() if not user_group: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Default user group not found." - ) + user_group = UserGroupModel(name=UserGroupEnum.USER) + db.add(user_group) + await db.commit() + await db.refresh(user_group) try: new_user = UserModel.create( @@ -120,6 +123,13 @@ async def register_user( await db.commit() await db.refresh(new_user) + activation_link = f"http://127.0.0.1/api/v1/accounts/activate/?token={activation_token.token}&email={user_data.email}" + + background_tasks.add_task( + email_sender.send_activation_email, + new_user.email, + activation_link, + ) except SQLAlchemyError as e: await db.rollback() raise HTTPException( @@ -163,7 +173,9 @@ async def register_user( ) async def activate_account( activation_data: UserActivationRequestSchema, + background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> MessageResponseSchema: """ Endpoint to activate a user's account. @@ -217,6 +229,14 @@ async def activate_account( user.is_active = True await db.delete(token_record) await db.commit() + await db.refresh(user) + + login_link = "http://127.0.0.1/login/" + background_tasks.add_task( + email_sender.send_activation_complete_email, + user.email, + login_link + ) return MessageResponseSchema(message="User account activated successfully.") @@ -233,7 +253,9 @@ async def activate_account( ) async def request_password_reset_token( data: PasswordResetRequestSchema, + background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> MessageResponseSchema: """ Endpoint to request a password reset token. @@ -262,6 +284,12 @@ async def request_password_reset_token( reset_token = PasswordResetTokenModel(user_id=cast(int, user.id)) db.add(reset_token) await db.commit() + reset_link = f"http://127.0.0.1/accounts/reset-password/complete/?token={reset_token.token}&email={user.email}" + background_tasks.add_task( + email_sender.send_password_reset_email, + user.email, + reset_link + ) return MessageResponseSchema( message="If you are registered, you will receive an email with instructions." @@ -313,7 +341,10 @@ async def request_password_reset_token( ) async def reset_password( data: PasswordResetCompleteRequestSchema, + background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), + ) -> MessageResponseSchema: """ Endpoint for resetting a user's password. @@ -360,6 +391,7 @@ async def reset_password( if expires_at < datetime.now(timezone.utc): await db.run_sync(lambda s: s.delete(token_record)) await db.commit() + raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid email or token." @@ -369,6 +401,13 @@ async def reset_password( user.password = data.password await db.run_sync(lambda s: s.delete(token_record)) await db.commit() + + login_link = "http://127.0.0.1/accounts/login/" + background_tasks.add_task( + email_sender.send_password_reset_complete_email, + user.email, + login_link + ) except SQLAlchemyError: await db.rollback() raise HTTPException( diff --git a/src/routes/profiles.py b/src/routes/profiles.py index 0b7c3420..08ee41fd 100644 --- a/src/routes/profiles.py +++ b/src/routes/profiles.py @@ -1,5 +1,205 @@ -from fastapi import APIRouter +from fastapi import ( + APIRouter, + Depends, + HTTPException, + status, + UploadFile, + File, + Request, +) +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from io import BytesIO + +from config import get_jwt_auth_manager +from database import get_db +from database.models.accounts import UserModel, UserProfileModel +from exceptions import TokenExpiredError, InvalidTokenError +from schemas.profiles import ProfileCreateSchema, ProfileResponseSchema +from security.token_manager import JWTAuthManager +from storages.interfaces import S3StorageInterface +from config.dependencies import get_s3_storage_client +from validation.profile import validate_image, validate_gender, validate_birth_date, validate_name +from fastapi.security import OAuth2PasswordBearer + router = APIRouter() -# Write your code here +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/accounts/login/") + +async def get_token_from_header(request: Request) -> str: + auth_header = request.headers.get("Authorization") + if not auth_header: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authorization header is missing", + ) + if not auth_header.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Authorization header format. Expected 'Bearer '", + ) + return auth_header.split(" ", 1)[1] + +async def get_current_user( + token: str = Depends(get_token_from_header), + db: AsyncSession = Depends(get_db), + jwt_manager: JWTAuthManager = Depends(get_jwt_auth_manager), +) -> UserModel: + try: + payload = jwt_manager.decode_access_token(token) + user_id = payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token: missing user_id" + ) + + stmt = select(UserModel).where(UserModel.id == user_id) + result = await db.execute(stmt) + user = result.scalars().first() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return user + + except TokenExpiredError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired." + ) + except InvalidTokenError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token." + ) + + +@router.post("/users/{user_id}/profile/", + response_model=ProfileResponseSchema, + status_code=status.HTTP_201_CREATED + ) +async def create_user_profile( + user_id: int, + profile_data: ProfileCreateSchema = Depends(ProfileCreateSchema.as_form), + avatar: UploadFile | None = File(None), + db: AsyncSession = Depends(get_db), + current_user: UserModel = Depends(get_current_user), + s3_client: S3StorageInterface = Depends(get_s3_storage_client), +): + stmt = select(UserModel).where(UserModel.id == user_id) + result = await db.execute(stmt) + user = result.scalars().first() + + if not user or not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or not active." + ) + + if current_user.id != user_id and current_user.group_id != 3: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission to edit this profile." + ) + + stmt = select(UserProfileModel).where(UserProfileModel.user_id == user_id) + result = await db.execute(stmt) + existing_profile = result.scalars().first() + if existing_profile: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User already has a profile." + ) + + try: + validate_name(profile_data.first_name) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"{profile_data.first_name} contains non-english letters." + ) + + try: + validate_name(profile_data.last_name) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"{profile_data.last_name} contains non-english letters." + ) + + + try: + validate_birth_date(profile_data.date_of_birth) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=str(e) + ) + + try: + validate_gender(profile_data.gender) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=str(e) + ) + + if not profile_data.info.strip(): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Info field cannot be empty or contain only spaces." + ) + + avatar_url = None + avatar_key = None + if avatar: + try: + validate_image(avatar) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=str(e) + ) + + avatar_key = f"avatars/{user_id}_avatar.jpg" + try: + content = await avatar.read() + await s3_client.upload_file(avatar_key, content) + avatar_url = await s3_client.get_file_url(avatar_key) + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to upload avatar. Please try again later." + ) + + + new_profile = UserProfileModel( + user_id=user_id, + first_name=profile_data.first_name.lower(), + last_name=profile_data.last_name.lower(), + gender=profile_data.gender, + date_of_birth=profile_data.date_of_birth, + info=profile_data.info, + avatar=avatar_key, + ) + db.add(new_profile) + await db.commit() + await db.refresh(new_profile) + + response = ProfileResponseSchema( + id=new_profile.id, + user_id=new_profile.user_id, + first_name=new_profile.first_name, + last_name=new_profile.last_name, + gender=new_profile.gender, + date_of_birth=new_profile.date_of_birth, + info=new_profile.info, + avatar=avatar_url, + ) + + return response diff --git a/src/schemas/profiles.py b/src/schemas/profiles.py index adbcbcee..c308b584 100644 --- a/src/schemas/profiles.py +++ b/src/schemas/profiles.py @@ -1,13 +1,51 @@ from datetime import date - -from fastapi import UploadFile, Form, File, HTTPException +from fastapi import Form from pydantic import BaseModel, field_validator, HttpUrl +from pydantic_core import PydanticCustomError + -from validation import ( +from validation.profile import ( validate_name, - validate_image, validate_gender, validate_birth_date ) -# Write your code here +class ProfileCreateSchema(BaseModel): + first_name: str + last_name: str + gender: str + date_of_birth: date + info: str + + @classmethod + def as_form( + cls, + first_name: str = Form(...), + last_name: str = Form(...), + gender: str = Form(...), + date_of_birth: date = Form(...), + info: str = Form(...), + ): + return cls( + first_name=first_name, + last_name=last_name, + gender=gender, + date_of_birth=date_of_birth, + info=info, + ) + + + +class ProfileResponseSchema(BaseModel): + id: int + user_id: int + first_name: str + last_name: str + gender: str + date_of_birth: date + info: str + avatar: HttpUrl | None + + model_config = { + "from_attributes": True + } \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ce8ed03f..981c91c3 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,6 +1,6 @@ import pytest_asyncio from httpx import AsyncClient, ASGITransport -from sqlalchemy import insert +from sqlalchemy import insert, select from sqlalchemy.ext.asyncio import AsyncSession from config import get_settings, get_accounts_email_notificator, get_s3_storage_client @@ -8,7 +8,7 @@ reset_database, get_db_contextmanager, UserGroupEnum, - UserGroupModel + UserGroupModel, UserModel, ActivationTokenModel ) from database.populate import CSVDatabaseSeeder from main import app @@ -47,7 +47,7 @@ async def reset_db(request): yield -@pytest_asyncio.fixture(scope="session") +@pytest_asyncio.fixture(scope="session", autouse=True) async def reset_db_once_for_e2e(request): """ Reset the database once for end-to-end tests. @@ -56,6 +56,7 @@ async def reset_db_once_for_e2e(request): ensuring the database is reset before running E2E tests. """ await reset_database() + yield @pytest_asyncio.fixture(scope="session") @@ -177,7 +178,7 @@ async def jwt_manager() -> JWTAuthManagerInterface: ) -@pytest_asyncio.fixture(scope="function") +@pytest_asyncio.fixture(scope="function", autouse=True) async def seed_user_groups(db_session: AsyncSession): """ Asynchronously seed the UserGroupModel table with default user groups. @@ -185,13 +186,22 @@ async def seed_user_groups(db_session: AsyncSession): This fixture inserts all user groups defined in UserGroupEnum into the database and commits the transaction. It then yields the asynchronous database session for further testing. """ - groups = [{"name": group.value} for group in UserGroupEnum] - await db_session.execute(insert(UserGroupModel).values(groups)) + groups = [ + {"name": "USER"}, + {"name": "ADMIN"}, + ] + + for group in groups: + existing = await db_session.execute( + select(UserGroupModel).where(UserGroupModel.name == group["name"]) + ) + if not existing.scalars().first(): + await db_session.execute(insert(UserGroupModel).values(group)) + await db_session.commit() - yield db_session -@pytest_asyncio.fixture(scope="function") +@pytest_asyncio.fixture(scope="function", autouse=True) async def seed_database(db_session): """ Seed the database with test data if it is empty. @@ -209,3 +219,42 @@ async def seed_database(db_session): await seeder.seed() yield db_session + + +# @pytest_asyncio.fixture(scope="session", autouse=True) +# async def seed_test_user(e2e_db_session: AsyncSession): +# # гарантируем наличие групп +# for group_name in ["USER", "ADMIN"]: +# result = await e2e_db_session.execute( +# select(UserGroupModel).where(UserGroupModel.name == group_name) +# ) +# if not result.scalars().first(): +# e2e_db_session.add(UserGroupModel(name=group_name)) +# await e2e_db_session.commit() +# +# # создаём пользователя +# result = await e2e_db_session.execute( +# select(UserModel).where(UserModel.email == "test@mate.com") +# ) +# user = result.scalars().first() +# +# if not user: +# result_group = await e2e_db_session.execute( +# select(UserGroupModel).where(UserGroupModel.name == UserGroupEnum.USER) +# ) +# user_group = result_group.scalars().first() +# +# user = UserModel.create( +# email="test@mate.com", +# raw_password="StrongPassword123!", +# group_id=user_group.id, +# ) +# e2e_db_session.add(user) +# await e2e_db_session.flush() +# +# token = ActivationTokenModel(user_id=user.id) +# e2e_db_session.add(token) +# +# await e2e_db_session.commit() + + diff --git a/src/tests/test_e2e/test_email_notification.py b/src/tests/test_e2e/test_email_notification.py index 3d6bd281..2cf524e5 100644 --- a/src/tests/test_e2e/test_email_notification.py +++ b/src/tests/test_e2e/test_email_notification.py @@ -1,5 +1,5 @@ from email_validator import validate_email, EmailNotValidError -from sqlalchemy import select +from sqlalchemy import select, delete from sqlalchemy.orm import joinedload from validators import url as validate_url import pytest @@ -33,6 +33,20 @@ async def test_registration(e2e_client, reset_db_once_for_e2e, settings, seed_us - Verify that an email was sent to the expected recipient. - Ensure the email body contains the activation link. """ + + + # # Сначала удалить токены + # await e2e_db_session.execute(delete(ActivationTokenModel).where(ActivationTokenModel.user_id.in_( + # select(UserModel.id).where(UserModel.email == "test@mate.com") + # ))) + # await e2e_db_session.execute(delete(RefreshTokenModel).where(RefreshTokenModel.user_id.in_( + # select(UserModel.id).where(UserModel.email == "test@mate.com") + # ))) + # + # # Потом удалить пользователя + # await e2e_db_session.execute(delete(UserModel).where(UserModel.email == "test@mate.com")) + # await e2e_db_session.commit() + user_data = { "email": "test@mate.com", "password": "StrongPassword123!" diff --git a/src/tests/test_integration/test_movies.py b/src/tests/test_integration/test_movies.py index b774a64a..56c312f1 100644 --- a/src/tests/test_integration/test_movies.py +++ b/src/tests/test_integration/test_movies.py @@ -1,7 +1,7 @@ import random import pytest -from sqlalchemy import select, func +from sqlalchemy import select, func, delete from sqlalchemy.orm import joinedload from database import MovieModel @@ -11,13 +11,18 @@ LanguageModel, CountryModel ) +from tests.conftest import db_session @pytest.mark.asyncio -async def test_get_movies_empty_database(client): +async def test_get_movies_empty_database(client, db_session): """ Test that the `/movies/` endpoint returns a 404 error when the database is empty. """ + + await db_session.execute(delete(MovieModel)) + await db_session.commit() + response = await client.get("/api/v1/theater/movies/") assert response.status_code == 404, f"Expected 404, got {response.status_code}" @@ -250,7 +255,7 @@ async def test_get_movie_by_id_not_found(client): Test that the `/movies/{movie_id}` endpoint returns a 404 error when a movie with the given ID does not exist. """ - movie_id = 1 + movie_id = 9999 response = await client.get(f"/api/v1/theater/movies/{movie_id}/") assert response.status_code == 404, f"Expected status code 404, but got {response.status_code}"