From bcf0074f19d572ed82f89c93fa715c8ddcfe8ba2 Mon Sep 17 00:00:00 2001 From: Jukka Hassinen Date: Sun, 27 Oct 2024 15:35:42 +0000 Subject: [PATCH] update --- .pyre_configuration | 8 + .ruff.toml | 5 +- .watchmanconfig | 1 + Dockerfile | 13 + examples.py | 6 + noxfile.py | 4 +- poetry.lock | 574 +++++++++++++----------- pyproject.toml | 16 +- pytest-with-beartype.py | 35 ++ pytest-with-typeguard.py | 30 ++ superschema/__init__.py | 3 +- superschema/base.py | 71 +-- superschema/defaults.py | 4 + superschema/handlers/__init__.py | 8 +- superschema/handlers/auto.py | 10 + superschema/handlers/base.py | 81 +++- superschema/handlers/file.py | 3 +- superschema/handlers/numbers.py | 45 +- superschema/handlers/property.py | 21 +- superschema/handlers/relational.py | 49 +- superschema/handlers/text.py | 20 +- superschema/registry.py | 8 +- superschema/schema.py | 5 +- tests/conftest.py | 4 +- tests/settings.py | 3 +- tests/test_all.py | 372 --------------- tests/test_basics.py | 266 +++++++++++ tests/test_choices.py | 86 ++++ tests/test_error_messages.py | 28 ++ tests/test_id_fields.py | 59 +++ tests/test_number_fields.py | 97 ++++ tests/test_property_method_fields.py | 53 +++ tests/test_relations_are_supported.py | 381 ++++++++++++++++ tests/test_uuid_fields_are_supported.py | 28 ++ tests/utils.py | 114 +++++ 35 files changed, 1821 insertions(+), 690 deletions(-) create mode 100644 .pyre_configuration create mode 100644 .watchmanconfig create mode 100755 pytest-with-beartype.py create mode 100755 pytest-with-typeguard.py delete mode 100644 tests/test_all.py create mode 100644 tests/test_basics.py create mode 100644 tests/test_choices.py create mode 100644 tests/test_error_messages.py create mode 100644 tests/test_id_fields.py create mode 100644 tests/test_number_fields.py create mode 100644 tests/test_property_method_fields.py create mode 100644 tests/test_relations_are_supported.py create mode 100644 tests/test_uuid_fields_are_supported.py create mode 100644 tests/utils.py diff --git a/.pyre_configuration b/.pyre_configuration new file mode 100644 index 0000000..13d4286 --- /dev/null +++ b/.pyre_configuration @@ -0,0 +1,8 @@ +{ + "site_package_search_strategy": "all", + "source_directories": [ + "." + ], + "strict": true, + "python_version": "3.12" +} \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml index e84a884..b7a5fcc 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -2,4 +2,7 @@ target-version = "py312" [lint] select = ["ALL"] -external = ["WPS", "C", "W"] \ No newline at end of file +external = ["WPS", "C", "W"] + +[lint.per-file-ignores] +"tests/test_*.py" = ["S101"] \ No newline at end of file diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/Dockerfile b/Dockerfile index 7a52fbc..aa60e66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,24 @@ ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV POETRY_VIRTUALENVS_CREATE=0 +RUN apt-get update && apt-get install -y --no-install-recommends \ + watchman \ + && rm -rf /var/lib/apt/lists/* + RUN pip install --no-cache-dir -U pip setuptools wheel RUN pip install --no-cache-dir poetry +# Install pyenv for testing with different python versions +ENV PYENV_ROOT="$HOME/.pyenv" +ENV PATH="$PYENV_ROOT/bin:$PATH" +RUN curl https://pyenv.run | bash +RUN eval "$(pyenv init -)" +RUN pyenv install 3.13 +RUN pyenv local 3.13 + WORKDIR /app RUN --mount=type=bind,source=./pyproject.toml,target=/app/pyproject.toml \ --mount=type=bind,source=./poetry.lock,target=/app/poetry.lock \ poetry install --no-root + diff --git a/examples.py b/examples.py index dabad00..4acee10 100644 --- a/examples.py +++ b/examples.py @@ -74,6 +74,11 @@ class Meta: app_label = "tests" default_related_name = "books" + @property + def author_names(self) -> str: + """Return a comma separated list of author names.""" + return ", ".join([author.name for author in self.authors.all()]) + class Library(models.Model): name = models.CharField(max_length=100) @@ -137,6 +142,7 @@ class Meta(SuperSchema.Meta): "authors": {"name": Infer}, "publisher": {"name": Infer}, "book_copies": {"library": Infer}, # note: here we use a reverse relation + "author_names": Infer, } diff --git a/noxfile.py b/noxfile.py index 643ce5b..73d392e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,10 +6,10 @@ import nox -@nox.session +@nox.session(python=["3.12", "3.13"], reuse_venv=True) def tests(session: nox.Session) -> None: """Run the test suite.""" - session.install("pytest") + session.run("poetry", "install", "--only=test", external=True) session.run("pytest") diff --git a/poetry.lock b/poetry.lock index cbaa679..e8e940c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -135,13 +135,13 @@ yaml = ["PyYAML"] [[package]] name = "basedpyright" -version = "1.18.4" +version = "1.19.1" description = "static type checking for Python (but based)" optional = false python-versions = ">=3.8" files = [ - {file = "basedpyright-1.18.4-py3-none-any.whl", hash = "sha256:0cb4e192f66bfee9e8a34ce8c35f62e71c316b1c59a48e8d46c66dff268e19b4"}, - {file = "basedpyright-1.18.4.tar.gz", hash = "sha256:d32f8c3abab50792a61039ff8ff5a2412b68d1c13b101d4b63756cb773d9828f"}, + {file = "basedpyright-1.19.1-py3-none-any.whl", hash = "sha256:976744146fcecb7413a726fbb8e52f4606738e02820a7dfbe310199f8915311e"}, + {file = "basedpyright-1.19.1.tar.gz", hash = "sha256:2863e619296ddd7f9f566c44e4fa8f781d727a83fa2da6ef62951e0b657655f6"}, ] [package.dependencies] @@ -305,73 +305,73 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.6.2" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, - {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, - {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, - {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, - {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, - {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, - {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, - {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, - {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, - {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, - {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, - {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, - {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, - {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, - {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, - {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.extras] @@ -505,24 +505,28 @@ version = "5.1.0" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" -files = [ - {file = "django_stubs-5.1.0-py3-none-any.whl", hash = "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40"}, - {file = "django_stubs-5.1.0.tar.gz", hash = "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5"}, -] +files = [] +develop = false [package.dependencies] asgiref = "*" django = "*" django-stubs-ext = ">=5.1.0" -mypy = {version = ">=1.11.0,<1.12.0", optional = true, markers = "extra == \"compatible-mypy\""} +mypy = {version = ">=1.12,<1.14", optional = true, markers = "extra == \"compatible-mypy\""} types-PyYAML = "*" typing-extensions = ">=4.11.0" [package.extras] -compatible-mypy = ["mypy (>=1.11.0,<1.12.0)"] +compatible-mypy = ["mypy (>=1.12,<1.14)"] oracle = ["oracledb"] redis = ["redis"] +[package.source] +type = "git" +url = "https://github.com/typeddjango/django-stubs.git" +reference = "master" +resolved_reference = "8de9d9a7b1ac40399ae7b05f1512fb5c72960ef9" + [[package]] name = "django-stubs-ext" version = "5.1.0" @@ -538,6 +542,26 @@ files = [ django = "*" typing-extensions = "*" +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + [[package]] name = "docopt" version = "0.6.2" @@ -559,6 +583,21 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +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"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "eradicate" version = "2.3.0" @@ -851,6 +890,18 @@ files = [ {file = "grimp-3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f83b85aad278dfcaf2bf27b9cfa6dd6533dd96ecc510ba3bc0141344686857f"}, {file = "grimp-3.5-cp312-none-win32.whl", hash = "sha256:f88307f0e50883ab73cc59164a5a9396e8e1c8b68b8e2edef68d478b91d81000"}, {file = "grimp-3.5-cp312-none-win_amd64.whl", hash = "sha256:6fa422c150597f8e6ad51c4fe2b271747057abe638acca5eebb2162e536065ed"}, + {file = "grimp-3.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:448dba63f938d0e13e6121559749816e3b2644202c912cc308e7608c6034737a"}, + {file = "grimp-3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:17113aba41269d0ee91512c96eeb850c7c668440c6a8e0bfc94d17762184b293"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a55614945c319d1dc692c3e43f3a02b80c116a1298e593f5f887b97e6c983a"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aba7ce7839b72efb4c6d06404d2b2a3026e28dd89816f4e546b3cd6626cbeeb1"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eaedfba320a70d87b14acb25a685c8629586b943129c71ffd02b47d9531c11ce"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60a9afd3dd00ad1d301a07f97e26bc9ecdc3d2db39ab6ac46c315a7dea0a96cb"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11c66039c0475e5c9fc6a086264f11870181ae79f603caa5dffa1411ddad636b"}, + {file = "grimp-3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bff39a0061790f074f86a7841cd8e6064aa7b2208cb1ee5c3f2e685dead2b66e"}, + {file = "grimp-3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf7f5367c4a87b8e9f08c09e7401d2d73f21cb65d6142445819f9df0d6ab3f6b"}, + {file = "grimp-3.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:edee4b087f007dab8b65461caf6a1b67b2f9480cceb5f6aceea87008d8f283c4"}, + {file = "grimp-3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6af125013ad2a56c18f2f53a3fcabbfbe96c70374abecd6f14b82dc444726ebe"}, + {file = "grimp-3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24aabae0183ca5fd5a710257ff37120b55d8e6d6d4cbb2c08481552832e5c901"}, {file = "grimp-3.5-cp313-none-win32.whl", hash = "sha256:506091bfd600dd7ad427586998ef5e54a2098485148a1499bd9af5943d2fb0b7"}, {file = "grimp-3.5-cp313-none-win_amd64.whl", hash = "sha256:099388df82d922ddc589f362f1a523ab053c8dee5d29a6b622b2cddf481c6a2f"}, {file = "grimp-3.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4bec3fbe008c8f8c546a640e6e74a519c9d33720c315c72013f1b6455543d1a9"}, @@ -895,13 +946,13 @@ typing-extensions = ">=3.10.0.0" [[package]] name = "hypothesis" -version = "6.114.1" +version = "6.115.5" description = "A library for property-based testing" optional = false python-versions = ">=3.9" files = [ - {file = "hypothesis-6.114.1-py3-none-any.whl", hash = "sha256:117f2d065d3f2ec5b2b37c89150c2350be7feb43dfb9ccc30f30d5293b34e607"}, - {file = "hypothesis-6.114.1.tar.gz", hash = "sha256:15ea6e4bb297276351ada18f172c60049c117a91040154ea620c632cc4c53e88"}, + {file = "hypothesis-6.115.5-py3-none-any.whl", hash = "sha256:b7733459ae9a93020fac3b91b41473c9b85e975139a152a70d88f3a5caa3fa3f"}, + {file = "hypothesis-6.115.5.tar.gz", hash = "sha256:4768c5fb426b305462ed31032d6e216a31daaefb1dc3134fdf2795b7961d7cb3"}, ] [package.dependencies] @@ -925,6 +976,20 @@ pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] zoneinfo = ["tzdata (>=2024.2)"] +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "immutabledict" version = "4.2.0" @@ -1223,92 +1288,92 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {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"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "marshmallow" -version = "3.22.0" +version = "3.23.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"}, - {file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"}, + {file = "marshmallow-3.23.0-py3-none-any.whl", hash = "sha256:82f20a2397834fe6d9611b241f2f7e7b680ed89c49f84728a1ad937be6b4bdf4"}, + {file = "marshmallow-3.23.0.tar.gz", hash = "sha256:98d8827a9f10c03d44ead298d2e99c6aea8197df18ccfad360dae7f89a50da2e"}, ] [package.dependencies] packaging = ">=17.0" [package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] -tests = ["pytest", "pytz", "simplejson"] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] +docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"] +tests = ["pytest", "simplejson"] [[package]] name = "marshmallow-enum" @@ -1414,38 +1479,43 @@ yaml = ["pyyaml"] [[package]] name = "mypy" -version = "1.11.2" +version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, - {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, - {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, - {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, - {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, - {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, - {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, - {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, - {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, - {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, - {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, - {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, - {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, - {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, - {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, - {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, - {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, - {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, - {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, - {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, - {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, - {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, - {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] @@ -1454,6 +1524,7 @@ typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -1482,13 +1553,13 @@ files = [ [[package]] name = "networkx" -version = "3.4" +version = "3.4.2" description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.10" files = [ - {file = "networkx-3.4-py3-none-any.whl", hash = "sha256:46dad0ec74a825a968e2b36c37ef5b91faa3868f017b2283d9cbff33112222ce"}, - {file = "networkx-3.4.tar.gz", hash = "sha256:1269b90f8f0d3a4095f016f49650f35ac169729f49b69d0572b2bb142748162b"}, + {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, + {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, ] [package.extras] @@ -1686,32 +1757,33 @@ wcwidth = "*" [[package]] name = "psutil" -version = "6.0.0" +version = "6.1.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, ] [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] [[package]] name = "ptyprocess" @@ -2014,13 +2086,13 @@ pylint = ">=1.7" [[package]] name = "pyparsing" -version = "3.1.4" +version = "3.2.0" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.9" files = [ - {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, - {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, ] [package.extras] @@ -2067,13 +2139,13 @@ typing-inspect = "*" [[package]] name = "pyright" -version = "1.1.384" +version = "1.1.386" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.384-py3-none-any.whl", hash = "sha256:f0b6f4db2da38f27aeb7035c26192f034587875f751b847e9ad42ed0c704ac9e"}, - {file = "pyright-1.1.384.tar.gz", hash = "sha256:25e54d61f55cbb45f1195ff89c488832d7a45d59f3e132f178fdf9ef6cafc706"}, + {file = "pyright-1.1.386-py3-none-any.whl", hash = "sha256:7071ac495593b2258ccdbbf495f1a5c0e5f27951f6b429bed4e8b296eb5cd21d"}, + {file = "pyright-1.1.386.tar.gz", hash = "sha256:8e9975e34948ba5f8e07792a9c9d2bdceb2c6c0b61742b068d2229ca2bc4a9d9"}, ] [package.dependencies] @@ -2236,29 +2308,29 @@ typing-extensions = ">=4.3.0" [[package]] name = "pywin32" -version = "307" +version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b"}, - {file = "pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d"}, - {file = "pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4"}, - {file = "pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75"}, - {file = "pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3"}, - {file = "pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398"}, - {file = "pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815"}, - {file = "pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347"}, - {file = "pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2"}, - {file = "pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5"}, - {file = "pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2"}, - {file = "pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a"}, - {file = "pywin32-307-cp37-cp37m-win32.whl", hash = "sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511"}, - {file = "pywin32-307-cp37-cp37m-win_amd64.whl", hash = "sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba"}, - {file = "pywin32-307-cp38-cp38-win32.whl", hash = "sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01"}, - {file = "pywin32-307-cp38-cp38-win_amd64.whl", hash = "sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b"}, - {file = "pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6"}, - {file = "pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87"}, + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] @@ -2459,13 +2531,13 @@ docutils = ">=0.11,<1.0" [[package]] name = "rich" -version = "13.9.2" +version = "13.9.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, - {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, + {file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, + {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, ] [package.dependencies] @@ -2477,40 +2549,40 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.9" +version = "0.7.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, - {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, - {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, - {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, - {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, - {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, - {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, + {file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"}, + {file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"}, + {file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"}, + {file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"}, + {file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"}, + {file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"}, + {file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"}, + {file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"}, + {file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"}, ] [[package]] name = "setuptools" -version = "75.1.0" +version = "75.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, + {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, + {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, ] [package.extras] @@ -2760,13 +2832,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.27.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, + {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, ] [package.dependencies] @@ -2867,4 +2939,4 @@ typing_extensions = ">=4.0,<5.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "61ca4ec88b0af823e7db5426c20b3e769291864febc01f10f658ad233e24ae76" +content-hash = "b3aeb184a364ed2237b60be4f4bd6919d09e7b5820a0e9b73c6af8badabcfb2f" diff --git a/pyproject.toml b/pyproject.toml index 474e287..196f8f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ classifiers = [ python = "^3.12" django = "^5.1.1" pydantic = "^2.9.2" +email-validator = "^2.2.0" [tool.poetry.group.dev.dependencies] pyright = "^1.1.383" @@ -52,10 +53,10 @@ mypy = "^1.11.2" wemake-python-styleguide = "^0.19.2" pytype = "^2024.9.13" isort = "^5.13.2" -django-stubs = {extras = ["compatible-mypy"], version = "^5.1.0"} +django-stubs = {git = "https://github.com/typeddjango/django-stubs.git", rev = "master", extras = ["compatible-mypy"]} pytest = "^8.3.3" pytest-cov = "^5.0.0" -ruff = "^0.6.9" +ruff = "^0.7.0" nox = "^2024.4.15" pytest-django = "^4.9.0" hypothesis = "^6.112.4" @@ -68,6 +69,17 @@ import-linter = "^2.1" debugpy = "^1.8.6" ipykernel = "^6.29.5" + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.3" +pytest-django = "^4.9.0" +hypothesis = "^6.115.5" +beartype = "^0.19.0" +typeguard = "^4.3.0" +pydantic = "^2.9.2" +email-validator = "^2.2.0" +django = "^5.1.2" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/pytest-with-beartype.py b/pytest-with-beartype.py new file mode 100755 index 0000000..b7da420 --- /dev/null +++ b/pytest-with-beartype.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +"""Pytest wrapper which instruments it with Beartype's type annotation checks. + +Why is this needed? +------------------ + +Because if you run pytest with the pytest-beartype plugin with command: +`pytest --beartype-packages='src,tests,automation,config'` +it will emit the following type warning: +``` +BeartypePytestWarning: Previously imported packages "..." not checkable by beartype. +``` + +This is because the Beartype plugin is not able to instrument the packages +that are already imported somehow by pytest. +Refs: +- https://github.com/beartype/beartype/issues/322 +- https://github.com/beartype/pytest-beartype/issues/3 + +So this wrapper script provides the workaround for this issue. + +""" + +import pytest +from beartype import BeartypeConf +from beartype.claw import beartype_package + +type_check_instrumented_packages: list[str] = ["superschema", "tests"] + +for package in type_check_instrumented_packages: + beartype_package(package_name=package, conf=BeartypeConf()) + + +# Run all tests in the current directory +pytest.main() diff --git a/pytest-with-typeguard.py b/pytest-with-typeguard.py new file mode 100755 index 0000000..34318ec --- /dev/null +++ b/pytest-with-typeguard.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +"""Pytest wrapper which instruments it with Typeguard's type annotation checks. + +Why is this needed? +------------------ + +Because if you run pytest with the pytest-typeguard plugin with command: +`pytest typeguard-packages=src,tests,automation,config` +it will emit the following type warning: +``` +InstrumentationWarning: +typeguard cannot check these packages because they are already imported: config, ... +```` + +This is because the Typeguard plugin is not able to instrument the packages +that are already imported somehow by pytest. + +So this wrapper script provides the workaround for this issue. + +""" + +import pytest +from typeguard import install_import_hook + +type_check_instrumented_packages: list[str] = ["superschema", "tests"] + +install_import_hook(packages=type_check_instrumented_packages) + +# Run all tests in the current directory +pytest.main() diff --git a/superschema/__init__.py b/superschema/__init__.py index 3184884..44ef5f8 100644 --- a/superschema/__init__.py +++ b/superschema/__init__.py @@ -1,5 +1,6 @@ """Super schema packages.""" +from superschema.schema import SuperSchema from superschema.types import Infer, InferExcept, MetaFields, ModelFields -__all__ = ["Infer", "InferExcept", "ModelFields", "MetaFields"] +__all__ = ["Infer", "InferExcept", "ModelFields", "MetaFields", "SuperSchema"] diff --git a/superschema/base.py b/superschema/base.py index 1f06038..8dbe958 100644 --- a/superschema/base.py +++ b/superschema/base.py @@ -1,14 +1,15 @@ """Tooling to convert Django models and fields to Pydantic native models.""" from collections.abc import Callable -from enum import Enum +from enum import Enum, IntEnum +from types import UnionType from typing import Any, Optional, TypeVar, override from uuid import UUID from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import FieldDoesNotExist from django.db import models -from pydantic import BaseModel, create_model +from pydantic import BaseModel, ConfigDict, create_model from pydantic._internal._model_construction import ModelMetaclass from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined @@ -40,10 +41,19 @@ ] +def has_property(cls: type, property_name: str) -> bool: + return hasattr(cls, property_name) and isinstance( + getattr(cls, property_name), + property, + ) + + def create_pydantic_model( django_model: type[models.Model], field_type_registry: FieldTypeRegistry, included_fields: ModelFields, + model_name: str | None = None, + config: ConfigDict | None = None, ) -> type[BaseModel]: """Create a Pydantic model from a Django model. @@ -67,22 +77,29 @@ def create_pydantic_model( """ pydantic_fields: dict[ str, - tuple[type[BaseModel | list[BaseModel]] | type | Enum, FieldInfo], + tuple[ + type[BaseModel | list[BaseModel]] | type | Enum | IntEnum | UnionType, + FieldInfo, + ], ] = {} errors: list[str] = [] for field_name, field_def in included_fields.items(): - try: - django_field = django_model._meta.get_field( # noqa: SLF001, WPS437 - field_name=field_name, - ) - except FieldDoesNotExist: - errors.append( - f"The fields definition includes field '{field_name}' " - f"which is not found in the Django model '{django_model.__name__}'.", - ) - continue + # Check if the field is a property function: + if has_property(django_model, field_name): + django_field = getattr(django_model, field_name) + else: + try: + django_field = django_model._meta.get_field( # noqa: SLF001, WPS437 + field_name=field_name, + ) + except FieldDoesNotExist: + errors.append( + f"The fields definition includes field '{field_name}' " + f"which is not found in the Django model '{django_model.__name__}'.", + ) + continue if field_def is Infer: type_handler = field_type_registry.get_handler(django_field) @@ -127,7 +144,6 @@ def create_pydantic_model( ), ) elif isinstance(field_def, dict): - print("field_def", field_def) related_django_model_name = field_name related_model_fields = field_def @@ -148,6 +164,7 @@ def create_pydantic_model( related_django_model, field_type_registry, related_model_fields, + model_name=f"{model_name}_{related_django_model_name}", ) default = PydanticUndefined @@ -192,15 +209,6 @@ def create_pydantic_model( ) continue - print( - "field_name", - field_name, - "field_type", - field_type, - "django_field", - django_field, - ) - pydantic_fields[related_django_model_name] = ( field_type, FieldInfo( @@ -218,11 +226,21 @@ def create_pydantic_model( ) if errors: - raise AttributeError("Error: ".join(errors)) + msg = ( + f"Error creating Pydantic model from '{django_model.__name__}' Django model:" + "\n\t❌ " + "\n\t❌ ".join(errors) + ) + raise AttributeError(msg) # Finally, create the Pydantic model: # https://docs.pydantic.dev/2.9/concepts/models/#dynamic-model-creation - return create_model(f"{django_model.__name__}Schema", **pydantic_fields) + model_name = model_name or f"{django_model.__name__}Schema" + return create_model( + model_name, + __config__={"from_attributes": True}, + **pydantic_fields, + ) Bases = tuple[type[BaseModel]] @@ -263,8 +281,11 @@ def __new__( # pylint: disable=W0222,C0204 msg = f"model field is required in Meta class for {name}" raise ValueError(msg) + model_name = getattr(namespace["Meta"], "name", None) return create_pydantic_model( model_class, field_type_registry, included_fields=namespace["Meta"].fields, + model_name=model_name, + config=namespace.get("model_config", None), ) diff --git a/superschema/defaults.py b/superschema/defaults.py index a98a939..4b33892 100644 --- a/superschema/defaults.py +++ b/superschema/defaults.py @@ -32,3 +32,7 @@ field_type_registry.register(handlers.BigIntegerFieldHandler) field_type_registry.register(handlers.SlugFieldHandler) field_type_registry.register(handlers.AutoFieldHandler) +field_type_registry.register(handlers.BigAutoFieldHandler) +field_type_registry.register(handlers.PropertyHandler) +field_type_registry.register(handlers.OneToOneFieldHandler) +field_type_registry.register(handlers.ManyToManyFieldHandler) diff --git a/superschema/handlers/__init__.py b/superschema/handlers/__init__.py index 4b91b19..8f16f41 100644 --- a/superschema/handlers/__init__.py +++ b/superschema/handlers/__init__.py @@ -26,7 +26,11 @@ SmallIntegerFieldHandler, ) from superschema.handlers.property import PropertyHandler -from superschema.handlers.relational import ForeignKeyHandler +from superschema.handlers.relational import ( + ForeignKeyHandler, + ManyToManyFieldHandler, + OneToOneFieldHandler, +) from superschema.handlers.text import ( CharFieldHandler, EmailFieldHandler, @@ -67,6 +71,8 @@ "AutoFieldHandler", "BigAutoFieldHandler", "ForeignKeyHandler", + "OneToOneFieldHandler", + "ManyToManyFieldHandler", "PropertyHandler", "JSONFieldHandler", "BinaryFieldHandler", diff --git a/superschema/handlers/auto.py b/superschema/handlers/auto.py index cc821c7..e269593 100644 --- a/superschema/handlers/auto.py +++ b/superschema/handlers/auto.py @@ -71,6 +71,16 @@ def ge(self) -> int | None: def le(self) -> int | None: return 9223372036854775807 + @property + @override + def examples(self) -> list[int]: + if self.ge is not None and self.le is not None: + return [self.ge, self.le] + if self.ge is not None: + return [self.ge] + if self.le is not None: + return [self.le] + @override def get_pydantic_type_raw(self) -> type[int]: return int diff --git a/superschema/handlers/base.py b/superschema/handlers/base.py index 4786974..d984151 100644 --- a/superschema/handlers/base.py +++ b/superschema/handlers/base.py @@ -18,6 +18,14 @@ from uuid import UUID from django.contrib.contenttypes.fields import GenericForeignKey +from django.core.validators import ( + MaxLengthValidator, + MaxValueValidator, + MinLengthValidator, + MinValueValidator, + RegexValidator, + StepValueValidator, +) from django.db import models from django.utils.encoding import force_str from pydantic import Field @@ -32,13 +40,14 @@ | models.ForeignObjectRel | GenericForeignKey | Callable[[], type] + | property ) TFieldType_co = TypeVar("TFieldType_co", bound=SupportedParentFields, covariant=True) @runtime_checkable -class PydanticConverter(Generic[TFieldType_co], Protocol): +class PydanticConverter(Protocol, Generic[TFieldType_co]): """Define the interface for a Pydantic field converter.""" def __init__(self, field_obj: TFieldType_co) -> None: @@ -171,6 +180,11 @@ def deprecated(self) -> bool | str: """Return whether the field is deprecated.""" return False + @property + def multiple_of(self) -> int | float | None: + """Return the multiple of the field.""" + return None + @abstractmethod def get_pydantic_type(self) -> IntEnum | Enum | UnionType | type: """Return the Pydantic type of the field.""" @@ -193,6 +207,7 @@ def get_pydantic_field(self) -> FieldInfo: decimal_places=self.decimal_places, min_length=self.min_length, max_length=self.max_length, + multiple_of=self.multiple_of, deprecated=self.deprecated, ) @@ -205,7 +220,7 @@ def get_pydantic_field(self) -> FieldInfo: TDjangoField = TypeVar("TDjangoField", bound=models.Field[Any, Any]) -class DjangoFieldHandler[T: models.Field[Any, Any]](FieldTypeHandler[T]): +class DjangoFieldHandler[T: models.Field[Any, Any]](FieldTypeHandler[T], ABC): """Base class for handling Django fields. Implementations should override the `field` class method to return the Django field class they handle. @@ -276,6 +291,68 @@ def examples(self) -> list[Any] | None: return [self.field_obj.default()] return None + @property + @override + def ge(self) -> int | None: + # check if the field has MinValueValidator + if self.field_obj.validators: + for validator in self.field_obj.validators: + if isinstance(validator, MinValueValidator): + return cast(int, validator.limit_value) + return None + + @property + @override + def le(self) -> int | None: + if self.field_obj.validators: + for validator in self.field_obj.validators: + if isinstance(validator, MaxValueValidator): + return cast(int, validator.limit_value) + + return None + + @property + @override + def multiple_of(self) -> int | None: + if self.field_obj.validators: + for validator in self.field_obj.validators: + if isinstance(validator, StepValueValidator): + if not getattr(validator, "offset", None): + return cast(int, validator.limit_value) + return None + + @property + @override + def max_length(self) -> int | None: + """Return the max length of the field if it has a MaxLengthValidator.""" + for validator in self.field_obj.validators: + if isinstance(validator, MaxLengthValidator): + if callable(validator.limit_value): + return cast(int, validator.limit_value()) + return cast(int, validator.limit_value) + return None + + @property + @override + def min_length(self) -> int | None: + """Return the min length of the field if it has a MinLengthValidator.""" + for validator in self.field_obj.validators: + if isinstance(validator, MinLengthValidator): + if callable(validator.limit_value): + return cast(int, validator.limit_value()) + return cast(int, validator.limit_value) + return None + + @property + @override + def pattern(self) -> re.Pattern[str] | str | None: + """Return the pattern of the field if it has a RegexValidator.""" + # Check if the Django field has any RegexValidator + for validator in self.field_obj.validators: + if isinstance(validator, RegexValidator): + return validator.regex + return None + @abstractmethod def get_pydantic_type_raw(self) -> type: """Return the type of the field.""" diff --git a/superschema/handlers/file.py b/superschema/handlers/file.py index d8215ec..2ce4091 100644 --- a/superschema/handlers/file.py +++ b/superschema/handlers/file.py @@ -3,6 +3,7 @@ from typing import override from django.db import models +from pydantic import FilePath from superschema.handlers.base import DjangoFieldHandler @@ -30,7 +31,7 @@ def field(cls) -> type[models.FilePathField[str]]: @override def get_pydantic_type_raw(self) -> type[str]: - return str + return FilePath class ImageFieldHandler(DjangoFieldHandler[models.ImageField]): diff --git a/superschema/handlers/numbers.py b/superschema/handlers/numbers.py index a1ab621..d8ff3fc 100644 --- a/superschema/handlers/numbers.py +++ b/superschema/handlers/numbers.py @@ -1,7 +1,7 @@ from decimal import Decimal from typing import override -from django.db import models +from django.db import connection, models from superschema.handlers.base import DjangoFieldHandler @@ -17,11 +17,12 @@ def field(cls) -> type[models.IntegerField[int]]: @property @override def ge(self) -> int | None: - return -2147483648 + return max(connection.ops.integer_field_range("IntegerField")[0], super().ge) @property + @override def le(self) -> int | None: - return 2147483647 + return min(connection.ops.integer_field_range("IntegerField")[1], super().le) @override def get_pydantic_type_raw(self) -> type[int]: @@ -39,12 +40,16 @@ def field(cls) -> type[models.SmallIntegerField[int]]: @property @override def ge(self) -> int | None: - return -32768 + return max( + connection.ops.integer_field_range("SmallIntegerField")[0], super().ge + ) @property @override def le(self) -> int | None: - return 32767 + return min( + connection.ops.integer_field_range("SmallIntegerField")[1], super().le + ) @override def get_pydantic_type_raw(self) -> type[int]: @@ -67,12 +72,18 @@ def field(cls) -> type[models.PositiveSmallIntegerField[int]]: @property @override def ge(self) -> int | None: - return 0 + return max( + connection.ops.integer_field_range("PositiveSmallIntegerField")[0], + super().ge, + ) @property @override def le(self) -> int | None: - return 32767 + return min( + connection.ops.integer_field_range("PositiveSmallIntegerField")[1], + super().le, + ) @override def get_pydantic_type_raw(self) -> type[int]: @@ -90,12 +101,16 @@ def field(cls) -> type[models.PositiveIntegerField[int]]: @property @override def ge(self) -> int | None: - return 0 + return max( + connection.ops.integer_field_range("PositiveIntegerField")[0], super().ge + ) @property @override def le(self) -> int | None: - return 2147483647 + return min( + connection.ops.integer_field_range("PositiveIntegerField")[1], super().le + ) @override def get_pydantic_type_raw(self) -> type[int]: @@ -113,12 +128,12 @@ def field(cls) -> type[models.BigIntegerField[int]]: @property @override def ge(self) -> int | None: - return -9223372036854775808 + return max(connection.ops.integer_field_range("BigIntegerField")[0], super().ge) @property @override def le(self) -> int | None: - return 9223372036854775807 + return min(connection.ops.integer_field_range("BigIntegerField")[1], super().le) @override def get_pydantic_type_raw(self) -> type[int]: @@ -138,12 +153,16 @@ def field(cls) -> type[models.PositiveBigIntegerField[int]]: @property @override def ge(self) -> int | None: - return 0 + return min( + connection.ops.integer_field_range("PositiveBigIntegerField")[0], super().ge + ) @property @override def le(self) -> int | None: - return 9223372036854775807 + return max( + connection.ops.integer_field_range("PositiveBigIntegerField")[1], super().le + ) @override def get_pydantic_type_raw(self) -> type[int]: diff --git a/superschema/handlers/property.py b/superschema/handlers/property.py index bdd3aec..8b1b0b9 100644 --- a/superschema/handlers/property.py +++ b/superschema/handlers/property.py @@ -1,6 +1,5 @@ """Handler for property decorated methods.""" -from collections.abc import Callable from typing import cast, override from pydantic import Field @@ -8,20 +7,28 @@ from superschema.handlers.base import FieldTypeHandler +ReturnType = str -class PropertyHandler(FieldTypeHandler[Callable[[], type]]): - """Handler for property decorated methods.""" - field_obj: Callable[[], type] +class PropertyHandler(FieldTypeHandler[property]): + """Handler for property decorated methods.""" @override @classmethod - def field(cls) -> Callable: - return Callable + def field(cls) -> type[property]: + return property + + @property + def deprecated(self) -> bool: + """Return whether the field is deprecated.""" + if self.field_obj.fget: + return getattr(self.field_obj.fget, "deprecated", False) @override - def get_pydantic_type_raw(self): + def get_pydantic_type(self) -> str: """Return the type of the property.""" + func = self.field_obj.fget + return func.__annotations__.get("return", None) @override def get_pydantic_field(self) -> FieldInfo: diff --git a/superschema/handlers/relational.py b/superschema/handlers/relational.py index ee46cda..c3d9b53 100644 --- a/superschema/handlers/relational.py +++ b/superschema/handlers/relational.py @@ -1,10 +1,11 @@ """Handlers for relational fields.""" -from typing import Any, override +from typing import Annotated, Any, override from django.db import models from django.db.models.base import ModelBase from django.db.models.fields.related import RelatedField +from pydantic.fields import FieldInfo from superschema.handlers.base import DjangoFieldHandler from superschema.registry import FieldTypeRegistry @@ -89,3 +90,49 @@ def get_pydantic_type_raw(self): return ( FieldTypeRegistry.instance().get_handler(target_field).get_pydantic_type() ) + + +class ManyToManyFieldHandler( + DjangoFieldHandler[models.ManyToManyField[models.Model, models.Model]], +): + """Handler for ManyToMany fields.""" + + @override + @classmethod + def field(cls) -> type[models.ManyToManyField[models.Model, models.Model]]: + return models.ManyToManyField + + def _get_target_field(self) -> models.Field[Any, Any]: + if hasattr(self.field_obj, "to_field") and isinstance( + self.field_obj.to_field, + ModelBase, + ): + target_field = self.field_obj.related_model._meta.get_field( + self.field_obj.to_field, + ) + else: + target_field = self.field_obj.related_model._meta.pk + if not target_field: + msg = f"Related model {self.field_obj.related_model} does not have a primary key field." + raise ValueError( + msg, + ) # This should never happen, but just in case, we raise an error here. + return target_field + + @override + def get_pydantic_type_raw(self): + return ( + FieldTypeRegistry.instance() + .get_handler(self._get_target_field()) + .get_pydantic_type() + ) + + @override + def get_pydantic_type(self) -> type[list[Annotated[Any, Any]]]: + """Return the Pydantic type of the field.""" + field_info: FieldInfo = ( + FieldTypeRegistry.instance() + .get_handler(self._get_target_field()) + .get_pydantic_field() + ) + return list[Annotated[self.get_pydantic_type_raw(), field_info]] diff --git a/superschema/handlers/text.py b/superschema/handlers/text.py index fb598e1..d580b4b 100644 --- a/superschema/handlers/text.py +++ b/superschema/handlers/text.py @@ -2,10 +2,10 @@ import re import uuid -from typing import Annotated, override +from typing import Annotated, cast, override from uuid import UUID -from django.core.validators import RegexValidator +from django.core.validators import MinLengthValidator, RegexValidator from django.db import models from pydantic import UUID1, UUID3, UUID4, UUID5, AnyUrl, EmailStr @@ -25,6 +25,17 @@ def field(cls) -> type[models.CharField[str]]: def max_length(self) -> int | None: return self.field_obj.max_length + @property + @override + def min_length(self) -> int | None: + """Return the min length of the field if it has a MinLengthValidator.""" + for validator in self.field_obj.validators: + if isinstance(validator, MinLengthValidator): + if callable(validator.limit_value): + return cast(int, validator.limit_value()) + return cast(int, validator.limit_value) + return None + @property @override def pattern(self) -> re.Pattern[str] | str | None: @@ -123,6 +134,11 @@ def field(cls) -> type[models.URLField[str]]: def max_length(self) -> int | None: return self.field_obj.max_length or 200 + @property + @override + def pattern(self) -> None: + return None + @override def get_pydantic_type_raw(self) -> type[AnyUrl]: return AnyUrl diff --git a/superschema/registry.py b/superschema/registry.py index d6b7312..9516b9a 100644 --- a/superschema/registry.py +++ b/superschema/registry.py @@ -1,7 +1,7 @@ """Tooling to convert Django models and fields to Pydantic native models.""" from collections.abc import Callable -from typing import Any, ClassVar, Self, TypeVar, override +from typing import Any, ClassVar, TypeVar, override from uuid import UUID from django.contrib.contenttypes.fields import GenericForeignKey @@ -14,7 +14,7 @@ models.Field[Any, Any] | models.ForeignObjectRel | GenericForeignKey - | Callable[[], type] + | Callable[[], type[Any]] ) TFieldType_co = TypeVar("TFieldType_co", bound=SupportedParentFields, covariant=True) @@ -28,7 +28,7 @@ class FieldTypeRegistry: """Registry for Django field type handlers.""" - _instance: ClassVar[Self | None] = None + _instance: "ClassVar[FieldTypeRegistry | None]" = None @override def __init__(self) -> None: @@ -39,7 +39,7 @@ def __init__(self) -> None: ] = {} @classmethod - def instance(cls) -> Self: + def instance(cls) -> "FieldTypeRegistry": """Return the singleton instance of the registry.""" if cls._instance is None: cls._instance = cls() diff --git a/superschema/schema.py b/superschema/schema.py index 8f32c18..43a605c 100644 --- a/superschema/schema.py +++ b/superschema/schema.py @@ -15,5 +15,6 @@ class SuperSchema(BaseModel, metaclass=SuperSchemaResolver): class Meta: """Pydantic configuration.""" - models: Model - fields: ClassVar[ModelFields] + name: str | None = None + models: Model | None = None + fields: ClassVar[ModelFields | None] = None diff --git a/tests/conftest.py b/tests/conftest.py index 3c6ac49..dafed16 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,12 @@ django_stubs_ext.monkeypatch() -def pytest_configure(config): +def pytest_configure(config) -> None: settings.configure( ALLOWED_HOSTS=["*"], DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ - "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}, }, SITE_ID=1, SECRET_KEY="not very secret in tests", diff --git a/tests/settings.py b/tests/settings.py index c5ac8ff..975ac21 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,8 +1,9 @@ """Django settings file for the tests.""" import django_stubs_ext +from django.db import models -django_stubs_ext.monkeypatch() +django_stubs_ext.monkeypatch(extra_classes=[models.BinaryField, models.JSONField]) ALLOWED_HOSTS = ["*"] DEBUG_PROPAGATE_EXCEPTIONS = True diff --git a/tests/test_all.py b/tests/test_all.py deleted file mode 100644 index 8548521..0000000 --- a/tests/test_all.py +++ /dev/null @@ -1,372 +0,0 @@ -"""Test cases for Django models to Pydantic models conversion.""" - -import json -import uuid -from pathlib import Path -from random import randint -from string import printable -from typing import Any, ClassVar - -import pydantic -import pytest -from django.db import models -from django.utils.translation import gettext_lazy as _ -from hypothesis import given -from hypothesis import strategies as st -from rich import print_json - -from superschema.schema import SuperSchema -from superschema.types import Infer, MetaFields, ModelFields - -FieldClass = type[models.Field[Any, Any]] - -AutoFields: list[FieldClass] = [models.AutoField, models.BigAutoField] - -DjangoFieldTypes: list[FieldClass] = [ - models.BigIntegerField, - models.BooleanField, - models.CharField, - models.DateField, - models.DateTimeField, - models.DecimalField, - models.DurationField, - # models.EmailField, - models.FileField, - models.FilePathField, - models.FloatField, - models.ImageField, - models.IntegerField, - models.GenericIPAddressField, - models.PositiveIntegerField, - models.PositiveSmallIntegerField, - models.PositiveBigIntegerField, - models.SlugField, - models.SmallIntegerField, - models.TextField, - models.TimeField, - models.URLField, - models.UUIDField, - models.BinaryField, - models.JSONField, -] - - -DjangoRelationalFields = [ - models.ForeignKey, - models.OneToOneField, - models.ManyToManyField, -] - -""" -Tests -choices are set as enum -choices are set as examples -Different choice definitions are supported -decimal values are set correctly -Charfield max_length is set correctly -Charfield, etc pattern is set correctly -exception is raised on included field that is not found -Default value is set -Default factory is set -Foreignkey and One-to-one field column names are included -Related fields are included -Property methods are included -Property setters are included -Related fields primary key type is -EmailStr is set -Ipaddress -Urlfield -Jsonfield -Is extensible with custom type handler -Model documentation is used as schema documentation -""" - -DjangoField = models.Field[Any, Any] - -JSONValue = str | int | float | bool | None | list["JSONValue"] | dict[str, "JSONValue"] - - -def debug_json(json_data: JSONValue) -> None: # pragma: no cover - """Print pretty the JSON value with indentation.""" - json_str: str = json.dumps(json_data) - print_json(data=json_str, indent=4) - with Path("debug.json").open("w") as file: - file.write(json_str) - - -def django_model_factory( - fields: dict[str, DjangoField], -) -> type[models.Model]: - """Create a Django model with the given fields.""" - meta_attrs: dict[str, str] = { - "app_label": "tests", - } - - attrs = { - "__module__": "django.db.models", - **fields, - "Meta": type("Meta", (), meta_attrs), - } - model_name = f"TestModel{randint(0, 10000000)}" - return type(model_name, (models.Model,), attrs) - - -def pydantic_schema_from_field(field: DjangoField) -> type[pydantic.BaseModel]: - """Create a Pydantic model from a Django model field.""" - field_name = "field" - model: type[models.Model] = django_model_factory(fields={field_name: field}) - fields_def: ModelFields = {field_name: Infer} - meta_class_attrs: MetaFields = { - "model": model, - "fields": fields_def, - } - meta_class = type("Meta", (), dict(meta_class_attrs)) - - return type( - "Schema", - (SuperSchema,), - { - "Meta": meta_class, - }, - ) - - -def get_openapi_schema_from_field(field: DjangoField) -> dict[str, Any]: - """Get the OpenAPI schema from a Django model field.""" - schema: type[pydantic.BaseModel] = pydantic_schema_from_field(field) - return schema.model_json_schema() - - -@pytest.mark.parametrize("field", DjangoFieldTypes) -@given( - help_text=st.text(alphabet=printable).filter( - condition=lambda help_text: help_text != "", - ), -) -def test_field_description_is_set_from_help_text( - field: FieldClass, - help_text: str, -) -> None: - """Test that the field description is set from the help text if it exists.""" - openapi_schema = get_openapi_schema_from_field(field(help_text=help_text)) - assert ( - openapi_schema["properties"]["field"]["description"].strip() - == help_text.strip() - ) - - -@pytest.mark.parametrize("field", DjangoFieldTypes) -@given( - help_text=st.text(alphabet=printable).filter( - condition=lambda help_text: help_text != "", - ), -) -def test_field_description_is_set_from_lazy_translated_help_text( - field: FieldClass, - help_text: str, -) -> None: - """Test that the field description is set from the help text if it exists.""" - openapi_schema = get_openapi_schema_from_field(field(help_text=_(help_text))) - assert openapi_schema["properties"]["field"]["description"].strip().replace( - "\r", - "", - ).replace("\n", "") == help_text.strip().replace("\r", "").replace("\n", "") - - -@pytest.mark.parametrize("field", DjangoFieldTypes) -@given( - verbose_name=st.text(alphabet=printable).filter( - condition=lambda verbose_name: verbose_name != "", - ), -) -def test_field_description_is_set_from_verbose_name_if_no_help_text( - field: FieldClass, - verbose_name: str, -) -> None: - """Test that the field description is set from the verbose name if it exists.""" - openapi_schema = get_openapi_schema_from_field(field(verbose_name=verbose_name)) - assert ( - openapi_schema["properties"]["field"]["description"].strip().lower() - == verbose_name.strip().lower() - ) - - -@pytest.mark.parametrize("field", DjangoFieldTypes) -@given( - verbose_name=st.text(alphabet=printable).filter( - condition=lambda verbose_name: verbose_name != "", - ), -) -def test_field_description_is_set_from_lazy_translated_verbose_name_if_no_help_text( - field: FieldClass, - verbose_name: str, -) -> None: - """Test that the field description is set from the verbose name if it exists.""" - openapi_schema = get_openapi_schema_from_field(field(verbose_name=_(verbose_name))) - assert openapi_schema["properties"]["field"][ - "description" - ].strip().lower().strip().replace("\r", "").replace( - "\n", - "", - ) == verbose_name.strip().lower().strip().replace("\r", "").replace("\n", "") - - -def test_uuid_field_is_supported() -> None: - """Test that UUID fields are supported.""" - openapi_schema = get_openapi_schema_from_field(models.UUIDField()) - assert openapi_schema["properties"]["field"]["type"] == "string" - assert openapi_schema["properties"]["field"]["format"] == "uuid" - - -def test_uuid1_field_is_supported() -> None: - """Test that UUID fields are supported.""" - openapi_schema = get_openapi_schema_from_field(models.UUIDField(default=uuid.uuid1)) - assert openapi_schema["properties"]["field"]["type"] == "string" - assert openapi_schema["properties"]["field"]["format"] == "uuid1" - - -def test_uuid4_field_is_supported() -> None: - """Test that UUID fields are supported.""" - openapi_schema = get_openapi_schema_from_field(models.UUIDField(default=uuid.uuid4)) - assert openapi_schema["properties"]["field"]["type"] == "string" - assert openapi_schema["properties"]["field"]["format"] == "uuid4" - - -def test_field_tuple_choices_are_set_as_enum() -> None: - """Test that field choices are set as enum.""" - choices = [("a", "A"), ("b", "B"), ("c", "C")] - openapi_schema = get_openapi_schema_from_field(models.CharField(choices=choices)) - debug_json(openapi_schema) - assert openapi_schema["$defs"]["FieldEnum"]["enum"] == [ - choice[0] for choice in choices - ] - assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" - assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" - - -def test_field_enum_choices_are_set_as_enum() -> None: - """Test that field's enum choices are set as enum.""" - - class Choices(models.TextChoices): - AA = "a" - BB = "b" - CC = "c" - - openapi_schema = get_openapi_schema_from_field( - models.CharField(choices=Choices.choices), - ) - debug_json(openapi_schema) - assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values - assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" - assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" - - -def test_field_enum_choices_with_label_are_set_as_enum() -> None: - """Test that field's enum choices are set as enum.""" - - class Choices(models.TextChoices): - AA = "a", "Aaa" - BB = "b", "Bee" - CC = "c", "Cee" - - openapi_schema = get_openapi_schema_from_field( - models.CharField(choices=Choices.choices), - ) - debug_json(openapi_schema) - assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values - assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" - assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" - - -def test_field_enum_choices_with_translated_label_are_set_as_enum() -> None: - """Test that field's enum choices are set as enum.""" - - class Choices(models.TextChoices): - A = "a", _("Aaa") - B = "b", _("Bee") - C = "c", _("Cee") - - openapi_schema = get_openapi_schema_from_field( - models.CharField(choices=Choices.choices), - ) - debug_json(openapi_schema) - assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values - assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" - assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" - - -def test_field_integer_enum_choices_are_set_as_enum() -> None: - """Test that field's enum choices are set as enum.""" - - class Choices(models.IntegerChoices): - A = 1, "Aaa" - B = 2, "Bee" - C = 3, "Cee" - - openapi_schema = get_openapi_schema_from_field( - models.IntegerField(choices=Choices.choices), - ) - # print(openapi_schema) - debug_json(openapi_schema) - assert openapi_schema["properties"]["field"]["enum"] == Choices.values - assert openapi_schema["properties"]["field"]["type"] == "integer" - - -def test_foreign_key_field_to_primary_key_is_supported() -> None: - """Test that foreign key fields are supported.""" - related_model = django_model_factory(fields={}) - openapi_schema = get_openapi_schema_from_field( - models.ForeignKey(related_model, on_delete=models.CASCADE), - ) - assert openapi_schema["properties"]["field"]["type"] == "integer" - - -def test_foreign_key_field_with_to_field_is_supported() -> None: - """Test that foreign key fields are supported.""" - related_model = django_model_factory( - fields={"someid": models.SmallIntegerField(unique=True)}, - ) - openapi_schema = get_openapi_schema_from_field( - models.ForeignKey(related_model, on_delete=models.CASCADE, to_field="someid"), - ) - assert openapi_schema["properties"]["field"]["type"] == "integer" - - -def test_models_can_have_nested_models() -> None: - """Test that models can have nested models.""" - - class ModelA(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - - class ModelB(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - rel_a = models.ForeignKey(ModelA, on_delete=models.CASCADE) - - class SchemaB(SuperSchema): - """SchemaB class.""" - - class Meta(SuperSchema.Meta): - """Meta class.""" - - model = ModelB - fields: ClassVar[ModelFields] = { - "id": Infer, - "name": Infer, - "rel_a": { - "id": Infer, - "name": Infer, - }, - } - - openapi_schema = SchemaB.model_json_schema() - debug_json(openapi_schema) - assert openapi_schema["properties"]["rel_a"]["type"] == "object" - assert ( - openapi_schema["properties"]["rel_a"]["properties"]["id"]["type"] == "integer" - ) - assert ( - openapi_schema["$defs"]["ModelASchema"]["properties"]["name"]["type"] - == "string" - ) diff --git a/tests/test_basics.py b/tests/test_basics.py new file mode 100644 index 0000000..028932a --- /dev/null +++ b/tests/test_basics.py @@ -0,0 +1,266 @@ +"""Test cases for Django models to Pydantic models conversion.""" + +import uuid +from string import printable +from typing import Any, ClassVar + +import pytest +from django.db import models +from django.utils.translation import gettext_lazy as _ +from hypothesis import given +from hypothesis import strategies as st + +from superschema.schema import SuperSchema +from superschema.types import Infer, ModelFields +from tests.utils import debug_json, get_openapi_schema_from_field + +FieldClass = type[models.Field[Any, Any]] + +AutoFields: list[FieldClass] = [models.AutoField, models.BigAutoField] + +DjangoFieldTypes: list[FieldClass] = [ + models.BigIntegerField, + models.BooleanField, + models.CharField, + models.DateField, + models.DateTimeField, + models.DecimalField, + models.DurationField, + models.EmailField, + models.FileField, + models.FilePathField, + models.FloatField, + models.ImageField, + models.IntegerField, + models.GenericIPAddressField, + models.PositiveIntegerField, + models.PositiveSmallIntegerField, + models.PositiveBigIntegerField, + models.SlugField, + models.SmallIntegerField, + models.TextField, + models.TimeField, + models.URLField, + models.UUIDField, + models.BinaryField, + models.JSONField, + models.SmallAutoField, + models.BigAutoField, + models.AutoField, +] + +DjangoRelationalFields = [ + models.ForeignKey, + models.OneToOneField, + models.ManyToManyField, +] + + +def get_test_data_for_field( + field: FieldClass, +) -> str | int | float | bool | uuid.UUID | bytes | dict[str, str]: + """Get the test data for the given field.""" + datas = { + models.CharField: "test", + models.TextField: "test", + models.IntegerField: 100, + models.FloatField: 100.0, + models.BooleanField: True, + models.DateField: "2021-01-01", + models.DateTimeField: "2021-01-01T00:00:00", + models.DecimalField: 100.0, + models.DurationField: "1 day", + models.EmailField: "test@test.com", + models.FileField: "test.txt", + models.FilePathField: "test.txt", + models.ImageField: "test.jpg", + models.GenericIPAddressField: "192.168.0.1", + models.PositiveIntegerField: 100, + models.PositiveSmallIntegerField: 100, + models.PositiveBigIntegerField: 100, + models.SlugField: "test", + models.TimeField: "00:00:00", + models.URLField: "http://example.com", + models.UUIDField: str(uuid.uuid4()), + models.BinaryField: b"test", + models.JSONField: {"test": "test"}, + models.BigIntegerField: 100, + models.SmallIntegerField: 100, + } + return datas[field] + + +def is_correct_field_type_and_format( + field: FieldClass, + type: str, + format: str | None, +) -> bool: + """Check the given field type and format whether they match the desired OpenAPI types.""" + datas = { + models.CharField: {"type": "string", "format": None}, + models.TextField: {"type": "string", "format": None}, + models.IntegerField: {"type": "integer", "format": None}, + models.FloatField: {"type": "number", "format": "float"}, + models.BooleanField: {"type": "boolean", "format": None}, + models.DateField: {"type": "string", "format": "date"}, + models.DateTimeField: {"type": "string", "format": "date-time"}, + models.DecimalField: {"type": "number", "format": "double"}, + models.DurationField: {"type": "string", "format": "duration"}, + models.BinaryField: {"type": "string", "format": "binary"}, + models.FileField: {"type": "string", "format": "file"}, + models.FilePathField: {"type": "string", "format": "file-path"}, + models.ImageField: {"type": "string", "format": "image"}, + models.GenericIPAddressField: {"type": "string", "format": "ipv4"}, + models.PositiveIntegerField: {"type": "integer", "format": None}, + models.PositiveSmallIntegerField: {"type": "integer", "format": None}, + models.PositiveBigIntegerField: {"type": "integer", "format": None}, + models.SlugField: {"type": "string", "format": None}, + models.TimeField: {"type": "string", "format": "time"}, + models.URLField: {"type": "string", "format": "uri"}, + models.UUIDField: {"type": "string", "format": "uuid"}, + models.BigIntegerField: {"type": "integer", "format": None}, + models.SmallIntegerField: {"type": "integer", "format": None}, + models.EmailField: {"type": "string", "format": "email"}, + models.JSONField: {"type": "object", "format": None}, + models.SmallAutoField: {"type": "integer", "format": None}, + models.AutoField: {"type": "integer", "format": None}, + models.BigAutoField: {"type": "integer", "format": None}, + } + return datas[field]["type"] == type and datas[field]["format"] == format + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +@given( + help_text=st.text(alphabet=printable).filter( + condition=lambda help_text: help_text != "", + ), +) +def test_field_description_is_set_from_help_text( + field: FieldClass, + help_text: str, +) -> None: + """Test that the field description is set from the help text if it exists.""" + openapi_schema = get_openapi_schema_from_field(field(help_text=help_text)) + assert ( + openapi_schema["properties"]["field"]["description"].strip() + == help_text.strip() + ) + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +def test_field_type_and_format_is_correct_openapi_equivalent(field: FieldClass) -> None: + """Test that the field type and format is correct OpenAPI equivalent.""" + openapi_schema = get_openapi_schema_from_field(field()) + field_format = openapi_schema["properties"]["field"].get("format", None) + assert is_correct_field_type_and_format( + field=field, + type=openapi_schema["properties"]["field"]["type"], + format=field_format, + ) + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +@given( + help_text=st.text(alphabet=printable).filter( + condition=lambda help_text: help_text != "", + ), +) +def test_field_description_is_set_from_lazy_translated_help_text( + field: FieldClass, + help_text: str, +) -> None: + """Test that the field description is set from the help text if it exists.""" + openapi_schema = get_openapi_schema_from_field(field(help_text=_(help_text))) + assert openapi_schema["properties"]["field"]["description"].strip().replace( + "\r", + "", + ).replace("\n", "") == help_text.strip().replace("\r", "").replace("\n", "") + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +@given( + verbose_name=st.text(alphabet=printable).filter( + condition=lambda verbose_name: verbose_name != "", + ), +) +def test_field_description_is_set_from_verbose_name_if_no_help_text( + field: FieldClass, + verbose_name: str, +) -> None: + """Test that the field description is set from the verbose name if it exists.""" + openapi_schema = get_openapi_schema_from_field(field(verbose_name=verbose_name)) + assert ( + openapi_schema["properties"]["field"]["description"].strip().lower() + == verbose_name.strip().lower() + ) + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +@given( + verbose_name=st.text(alphabet=printable).filter( + condition=lambda verbose_name: verbose_name != "", + ), +) +def test_field_description_is_set_from_lazy_translated_verbose_name_if_no_help_text( + field: FieldClass, + verbose_name: str, +) -> None: + """Test that the field description is set from the verbose name if it exists.""" + openapi_schema = get_openapi_schema_from_field(field(verbose_name=_(verbose_name))) + assert openapi_schema["properties"]["field"][ + "description" + ].strip().lower().strip().replace("\r", "").replace( + "\n", + "", + ) == verbose_name.strip().lower().strip().replace("\r", "").replace("\n", "") + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +def test_default_value_is_set(field: FieldClass) -> None: + """Test that the default value is set.""" + default_value = get_test_data_for_field(field) + openapi_schema = get_openapi_schema_from_field(field(default=default_value)) + assert openapi_schema["properties"]["field"]["default"] == default_value + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +def test_null_field_sets_field_as_not_required(field: FieldClass) -> None: + """Test that null fields are not required.""" + openapi_schema = get_openapi_schema_from_field(field(null=True)) + debug_json(openapi_schema) + assert openapi_schema.get("required", []) == [] + + +@pytest.mark.parametrize("field", DjangoFieldTypes) +def test_non_null_field_sets_as_required(field: FieldClass) -> None: + """Test that non-null fields are required.""" + openapi_schema = get_openapi_schema_from_field(field(null=False)) + debug_json(openapi_schema) + assert openapi_schema.get("required", []) == ["field"] + + +def test_schema_subclassing_works() -> None: + """Test that schema subclassing works.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + } + + class SchemaB(SchemaA): + """SchemaB class.""" + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["name"]["type"] == "string" diff --git a/tests/test_choices.py b/tests/test_choices.py new file mode 100644 index 0000000..375386b --- /dev/null +++ b/tests/test_choices.py @@ -0,0 +1,86 @@ +"""Test choices are set as enum.""" + +from django.db import models +from django.utils.translation import gettext as _ + +from tests.utils import debug_json, get_openapi_schema_from_field + + +def test_field_tuple_choices_are_set_as_enum() -> None: + """Test that field choices are set as enum.""" + choices = [("a", "A"), ("b", "B"), ("c", "C")] + openapi_schema = get_openapi_schema_from_field(models.CharField(choices=choices)) + debug_json(openapi_schema) + assert openapi_schema["$defs"]["FieldEnum"]["enum"] == [ + choice[0] for choice in choices + ] + assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" + assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" + + +def test_field_enum_choices_are_set_as_enum() -> None: + """Test that field's enum choices are set as enum.""" + + class Choices(models.TextChoices): + AA = "a" + BB = "b" + CC = "c" + + openapi_schema = get_openapi_schema_from_field( + models.CharField(choices=Choices.choices), + ) + debug_json(openapi_schema) + assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values + assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" + assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" + + +def test_field_enum_choices_with_label_are_set_as_enum() -> None: + """Test that field's enum choices are set as enum.""" + + class Choices(models.TextChoices): + AA = "a", "Aaa" + BB = "b", "Bee" + CC = "c", "Cee" + + openapi_schema = get_openapi_schema_from_field( + models.CharField(choices=Choices.choices), + ) + debug_json(openapi_schema) + assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values + assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" + assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" + + +def test_field_enum_choices_with_translated_label_are_set_as_enum() -> None: + """Test that field's enum choices are set as enum.""" + + class Choices(models.TextChoices): + A = "a", _("Aaa") + B = "b", _("Bee") + C = "c", _("Cee") + + openapi_schema = get_openapi_schema_from_field( + models.CharField(choices=Choices.choices), + ) + debug_json(openapi_schema) + assert openapi_schema["$defs"]["FieldEnum"]["enum"] == Choices.values + assert openapi_schema["properties"]["field"]["$ref"] == "#/$defs/FieldEnum" + assert openapi_schema["$defs"]["FieldEnum"]["type"] == "string" + + +def test_field_integer_enum_choices_are_set_as_enum() -> None: + """Test that field's enum choices are set as enum.""" + + class Choices(models.IntegerChoices): + A = 1, "Aaa" + B = 2, "Bee" + C = 3, "Cee" + + openapi_schema = get_openapi_schema_from_field( + models.IntegerField(choices=Choices.choices), + ) + # print(openapi_schema) + debug_json(openapi_schema) + assert openapi_schema["properties"]["field"]["enum"] == Choices.values + assert openapi_schema["properties"]["field"]["type"] == "integer" diff --git a/tests/test_error_messages.py b/tests/test_error_messages.py new file mode 100644 index 0000000..5e00b54 --- /dev/null +++ b/tests/test_error_messages.py @@ -0,0 +1,28 @@ +"""Test error messages.""" + +from typing import ClassVar + +import pytest +from django.db import models + +from superschema.schema import SuperSchema +from superschema.types import Infer, ModelFields + + +def test_defing_a_non_existing_field_raises_exception() -> None: + """Test that an exception is raised when an included field is not found.""" + with pytest.raises(AttributeError): # noqa: PT012 + + class ModelA(models.Model): # noqa: DJ008 + id = models.AutoField(primary_key=True) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelFields] = { + "non_existing_field": Infer, + } diff --git a/tests/test_id_fields.py b/tests/test_id_fields.py new file mode 100644 index 0000000..756187f --- /dev/null +++ b/tests/test_id_fields.py @@ -0,0 +1,59 @@ +"""Test that implicit id fields are supported.""" + +from typing import ClassVar + +from django.db import models +from pydantic.v1.fields import ModelField + +from superschema.schema import SuperSchema +from superschema.types import Infer +from tests.utils import debug_json + + +def test_implicit_id_fields_works() -> None: + """Test that implicit id fields are supported.""" + + class ModelA(models.Model): + name = models.CharField(max_length=100) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelField] = { + "id": Infer, + "name": Infer, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["id"]["type"] == "integer" + assert openapi_schema["properties"]["name"]["type"] == "string" + + +def test_explicit_id_fields_works() -> None: + """Test that explicit id fields are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelField] = { + "id": Infer, + "name": Infer, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["id"]["type"] == "integer" + assert openapi_schema["properties"]["name"]["type"] == "string" diff --git a/tests/test_number_fields.py b/tests/test_number_fields.py new file mode 100644 index 0000000..a4fd2a7 --- /dev/null +++ b/tests/test_number_fields.py @@ -0,0 +1,97 @@ +"""Number fields tests.""" + +import pydantic +import pytest +from django.core.validators import ( + MaxValueValidator, + MinValueValidator, + StepValueValidator, +) +from django.db import models + +from tests.utils import pydantic_schema_from_field + +IntegerField = type[ + models.SmallIntegerField[int] + | models.IntegerField[int] + | models.BigIntegerField[int] + | models.PositiveBigIntegerField[int] + | models.PositiveIntegerField[int] + | models.PositiveSmallIntegerField[int] + | models.SmallAutoField + | models.AutoField + | models.BigAutoField +] + +IntegerFields: list[IntegerField] = [ + models.SmallIntegerField[int], + models.IntegerField[int], + models.BigIntegerField[int], + models.PositiveBigIntegerField[int], + models.PositiveIntegerField[int], + models.PositiveSmallIntegerField[int], + models.SmallAutoField, + models.AutoField, + models.BigAutoField, +] + + +@pytest.mark.parametrize("django_field", IntegerFields) +def test_integer_field_min_value_validator_is_supported( + django_field: IntegerField, +) -> None: + """Test that the minimum value of an integer field is set.""" + field = django_field( + validators=[MinValueValidator(10)], + ) + + pydantic_model = pydantic_schema_from_field(field) + openapi_schema = pydantic_model.model_json_schema() + assert openapi_schema["properties"]["field"]["minimum"] == 10 + with pytest.raises(pydantic.ValidationError): + assert pydantic_model(field=9) + + +@pytest.mark.parametrize("django_field", IntegerFields) +def test_integer_field_max_value_validator_is_supported( + django_field: IntegerField, +) -> None: + """Test that the maximum value of an integer field is set.""" + field = django_field( + validators=[MaxValueValidator(10)], + ) + + pydantic_model = pydantic_schema_from_field(field) + openapi_schema = pydantic_model.model_json_schema() + assert openapi_schema["properties"]["field"]["maximum"] == 10 + with pytest.raises(pydantic.ValidationError): + assert pydantic_model(field=11) + + +@pytest.mark.parametrize("django_field", IntegerFields) +def test_step_value_validator_is_supported( + django_field: IntegerField, +) -> None: + """Test that the step value of an integer field is set.""" + field = django_field( + validators=[StepValueValidator(2, offset=0)], + ) + + pydantic_model = pydantic_schema_from_field(field) + openapi_schema = pydantic_model.model_json_schema() + assert openapi_schema["properties"]["field"]["multipleOf"] == 2 + + +@pytest.mark.parametrize("django_field", IntegerFields) +def test_example_value_is_set_based_on_le_and_or_ge( + django_field: IntegerField, +) -> None: + """Test that the example value is set based on the le and or ge values.""" + field = django_field( + validators=[MinValueValidator(11), MaxValueValidator(20)], + ) + + pydantic_model = pydantic_schema_from_field(field) + openapi_schema = pydantic_model.model_json_schema() + assert openapi_schema["properties"]["field"]["example"] == [11, 20] + assert pydantic_model(field=15) diff --git a/tests/test_property_method_fields.py b/tests/test_property_method_fields.py new file mode 100644 index 0000000..8c150b8 --- /dev/null +++ b/tests/test_property_method_fields.py @@ -0,0 +1,53 @@ +"""Test property method based fields.""" + +from typing import Any, ClassVar + +import pytest +from django.db import models + +from superschema.schema import SuperSchema +from superschema.types import Infer, ModelFields +from tests.utils import add_property_method, debug_json, get_openapi_equivalent + +BasicTypes = [ + int, + str, + float, + bool, + list[int], + list[str], + list[float], + list[bool], +] + + +@pytest.mark.parametrize("return_type", BasicTypes) +def test_django_model_property_methods_are_supported(return_type: Any) -> None: + property_name = "some_property" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + + add_property_method( + cls=ModelA, + name=property_name, + return_type=return_type, + value="test", + ) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelFields] = { + property_name: Infer, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"][property_name][ + "type" + ] == get_openapi_equivalent(python_native_type=return_type) diff --git a/tests/test_relations_are_supported.py b/tests/test_relations_are_supported.py new file mode 100644 index 0000000..df00a78 --- /dev/null +++ b/tests/test_relations_are_supported.py @@ -0,0 +1,381 @@ +import uuid +from typing import ClassVar + +from django.db import models + +from superschema.schema import SuperSchema +from superschema.types import Infer, ModelFields +from tests.utils import debug_json, django_model_factory, get_openapi_schema_from_field + + +def test_foreign_key_field_to_primary_key_is_supported() -> None: + """Test that foreign key fields are supported.""" + related_model = django_model_factory(fields={}) + openapi_schema = get_openapi_schema_from_field( + models.ForeignKey(related_model, on_delete=models.CASCADE), + ) + assert openapi_schema["properties"]["field"]["type"] == "integer" + + +def test_foreign_key_field_with_to_field_is_supported() -> None: + """Test that foreign key fields are supported.""" + related_model = django_model_factory( + fields={"someid": models.SmallIntegerField(unique=True)}, + ) + openapi_schema = get_openapi_schema_from_field( + models.ForeignKey(related_model, on_delete=models.CASCADE, to_field="someid"), + ) + assert openapi_schema["properties"]["field"]["type"] == "integer" + + +def test_foreign_key_field() -> None: + """Test that models can have nested models.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ForeignKey(ModelA, on_delete=models.CASCADE) + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": { + "id": Infer, + "var": Infer, + }, + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_a"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["var"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_foreign_key_field_with_to_field_works() -> None: + """Test that foreign key fields are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100, unique=True) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ForeignKey(ModelA, on_delete=models.CASCADE, to_field="var") + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": { + "id": Infer, + "var": Infer, + }, + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_a"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["var"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_many_to_many_field_works() -> None: + """Test that many to many fields are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ManyToManyField(ModelA) + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": { + "id": Infer, + "var": Infer, + }, + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["rel_a"]["type"] == "array" + assert ( + openapi_schema["properties"]["rel_a"]["items"]["properties"]["id"]["type"] + == "integer" + ) + assert ( + openapi_schema["$defs"]["ModelASchema"]["properties"]["name"]["type"] + == "string" + ) + + +def test_one_to_one_field_works() -> None: + """Test that one to one fields are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.OneToOneField(ModelA, on_delete=models.CASCADE) + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": { + "id": Infer, + "var": Infer, + }, + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_a"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["var"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_many_to_one_relations_work() -> None: + """Test that many to one relations are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ForeignKey(ModelA, on_delete=models.CASCADE) + + class Meta: + default_related_name = "rel_b" + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_b": { + "id": Infer, + "name": Infer, + }, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_b"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["name"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_many_to_many_reverse_relations_work() -> None: + """Test that many to many relations are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ManyToManyField(ModelA) + + class Meta: + default_related_name = "rel_b" + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelFields] = { + "id": Infer, + "var": Infer, + "rel_b": { + "id": Infer, + "name": Infer, + }, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_b"]["items"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["name"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_one_to_one_reverse_relations_work() -> None: + """Test that one to one relations are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + var = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.OneToOneField(ModelA, on_delete=models.CASCADE) + + class Meta: + default_related_name = "rel_b" + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "var": Infer, + "rel_b": { + "id": Infer, + "name": Infer, + }, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_b"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["name"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_relational_field_usage_by_id_works() -> None: + """Test that relational fields can be used by id.""" + + class ModelA(models.Model): + id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4) + name = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ForeignKey(ModelA, on_delete=models.CASCADE) + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a_id": Infer, # <--- This is the important part + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["rel_a_id"]["type"] == "string" + assert openapi_schema["properties"]["rel_a_id"]["format"] == "uuid4" + + +def test_symmetrical_many_to_many_fields_are_supported() -> None: + """Test that symmetrical many to many fields are supported.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ManyToManyField("self", symmetrical=True) + + class SchemaA(SuperSchema): + """SchemaA class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelA + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": { + "id": Infer, + "name": Infer, + }, + } + + openapi_schema = SchemaA.model_json_schema() + debug_json(openapi_schema) + ref = openapi_schema["properties"]["rel_a"]["items"]["$ref"].split("/")[-1] + assert openapi_schema["$defs"][ref]["properties"]["name"]["type"] == "string" + assert openapi_schema["$defs"][ref]["properties"]["id"]["type"] == "integer" + + +def test_many_to_many_relations_provide_an_array_of_ids() -> None: + """Test that many to many relations are represented as arrays.""" + + class ModelA(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + + class ModelB(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + rel_a = models.ManyToManyField(ModelA) + + class SchemaB(SuperSchema): + """SchemaB class.""" + + class Meta(SuperSchema.Meta): + """Meta class.""" + + model = ModelB + fields: ClassVar[ModelFields] = { + "id": Infer, + "name": Infer, + "rel_a": Infer, # <--- This is the important part + } + + openapi_schema = SchemaB.model_json_schema() + debug_json(openapi_schema) + assert openapi_schema["properties"]["rel_a"]["type"] == "array" + assert openapi_schema["properties"]["rel_a"]["items"]["type"] == "integer" diff --git a/tests/test_uuid_fields_are_supported.py b/tests/test_uuid_fields_are_supported.py new file mode 100644 index 0000000..7ac1f19 --- /dev/null +++ b/tests/test_uuid_fields_are_supported.py @@ -0,0 +1,28 @@ +"""Tests for UUID fields.""" + +import uuid + +from django.db import models + +from tests.utils import get_openapi_schema_from_field + + +def test_uuid_field_is_supported() -> None: + """Test that UUID fields are supported.""" + openapi_schema = get_openapi_schema_from_field(models.UUIDField()) + assert openapi_schema["properties"]["field"]["type"] == "string" + assert openapi_schema["properties"]["field"]["format"] == "uuid" + + +def test_uuid1_field_is_supported() -> None: + """Test that UUID fields are supported.""" + openapi_schema = get_openapi_schema_from_field(models.UUIDField(default=uuid.uuid1)) + assert openapi_schema["properties"]["field"]["type"] == "string" + assert openapi_schema["properties"]["field"]["format"] == "uuid1" + + +def test_uuid4_field_is_supported() -> None: + """Test that UUID fields are supported.""" + openapi_schema = get_openapi_schema_from_field(models.UUIDField(default=uuid.uuid4)) + assert openapi_schema["properties"]["field"]["type"] == "string" + assert openapi_schema["properties"]["field"]["format"] == "uuid4" diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..4fa9a72 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,114 @@ +"""Utility functions for testing.""" + +import json +from pathlib import Path +from random import randint +from typing import Any + +import pydantic +from django.db import models +from rich import print_json + +from superschema.schema import SuperSchema +from superschema.types import Infer, MetaFields, ModelFields + +DjangoField = models.Field[Any, Any] + +JSONValue = str | int | float | bool | None | list["JSONValue"] | dict[str, "JSONValue"] + + +def debug_json(json_data: JSONValue) -> None: # pragma: no cover + """Print pretty the JSON value with indentation.""" + json_str: str = json.dumps(json_data) + print_json(data=json_str, indent=4) + with Path("debug.json").open("w") as file: + file.write(json_str) + + +def django_model_factory( + fields: dict[str, DjangoField], +) -> type[models.Model]: + """Create a Django model with the given fields.""" + meta_attrs: dict[str, str] = { + "app_label": "tests", + } + + attrs = { + "__module__": "django.db.models", + **fields, + "Meta": type("Meta", (), meta_attrs), + } + model_name = f"TestModel{randint(0, 10000000)}" + return type(model_name, (models.Model,), attrs) + + +def pydantic_schema_from_field(field: DjangoField) -> type[pydantic.BaseModel]: + """Create a Pydantic model from a Django model field.""" + field_name = "field" + model: type[models.Model] = django_model_factory(fields={field_name: field}) + fields_def: ModelFields = {field_name: Infer} + meta_class_attrs: MetaFields = { + "model": model, + "fields": fields_def, + } + meta_class = type("Meta", (), dict(meta_class_attrs)) + + return type( + "Schema", + (SuperSchema,), + { + "Meta": meta_class, + }, + ) + + +def get_openapi_schema_from_field(field: DjangoField) -> dict[str, Any]: + """Get the OpenAPI schema from a Django model field.""" + schema: type[pydantic.BaseModel] = pydantic_schema_from_field(field) + return schema.model_json_schema() + + +def add_property_method( + cls: type[models.Model], + name: str, + return_type: type, + value: Any, +) -> None: + """Create a property method on the given class. + + Adds a property method to the given class with the given name, return type, and value. + + Args: + cls (type[models.Model]): The class to add the property method to. + name (str): The name of the property method. + return_type (type): The return type of the property method. + value (Any): The value to return from the property method. + + """ + + def property_method(self) -> Any: + """Test method.""" + + property_method.__annotations__["return"] = return_type + + setattr(cls, name, property(property_method)) + + +def get_openapi_equivalent(python_native_type: Any) -> Any: + """Get the OpenAPI equivalent of the given Python native type.""" + if python_native_type == int: + return "integer" + if python_native_type == str: + return "string" + if python_native_type == float: + return "number" + if python_native_type == bool: + return "boolean" + if python_native_type == list[int]: + return "array" + if python_native_type == list[str]: + return "array" + if python_native_type == list[float]: + return "array" + if python_native_type == list[bool]: + return "array"