├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── issue.yml ├── SECURITY.md ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .typos.toml ├── CHANGELOG.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pyproject.toml ├── src └── django_version_checks │ ├── __init__.py │ ├── apps.py │ ├── checks.py │ ├── py.typed │ └── typing.py ├── tests ├── __init__.py ├── settings.py └── test_checks.py ├── tox.ini └── uv.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.py] 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/). 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request an enhancement or new feature. 3 | body: 4 | - type: textarea 5 | id: description 6 | attributes: 7 | label: Description 8 | description: Please describe your feature request with appropriate detail. 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.yml: -------------------------------------------------------------------------------- 1 | name: Issue 2 | description: File an issue 3 | body: 4 | - type: input 5 | id: python_version 6 | attributes: 7 | label: Python Version 8 | description: Which version of Python were you using? 9 | placeholder: 3.9.0 10 | validations: 11 | required: false 12 | - type: input 13 | id: django_version 14 | attributes: 15 | label: Django Version 16 | description: Which version of Django were you using? 17 | placeholder: 3.2.0 18 | validations: 19 | required: false 20 | - type: input 21 | id: package_version 22 | attributes: 23 | label: Package Version 24 | description: Which version of this package were you using? If not the latest version, please check this issue has not since been resolved. 25 | placeholder: 1.0.0 26 | validations: 27 | required: false 28 | - type: textarea 29 | id: description 30 | attributes: 31 | label: Description 32 | description: Please describe your issue. 33 | validations: 34 | required: true 35 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | Please report security issues directly over email to me@adamj.eu 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | groups: 6 | "GitHub Actions": 7 | patterns: 8 | - "*" 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '**' 9 | pull_request: 10 | 11 | concurrency: 12 | group: ${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | tests: 17 | name: Python ${{ matrix.python-version }} 18 | runs-on: ubuntu-24.04 19 | 20 | strategy: 21 | matrix: 22 | python-version: 23 | - '3.9' 24 | - '3.10' 25 | - '3.11' 26 | - '3.12' 27 | - '3.13' 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - uses: actions/setup-python@v5 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | allow-prereleases: true 36 | 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@v6 39 | with: 40 | enable-cache: true 41 | 42 | - name: Install dependencies 43 | run: uv pip install --system tox tox-uv 44 | 45 | - name: Run tox targets for ${{ matrix.python-version }} 46 | run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) 47 | 48 | - name: Upload coverage data 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: coverage-data-${{ matrix.python-version }} 52 | path: '${{ github.workspace }}/.coverage.*' 53 | include-hidden-files: true 54 | if-no-files-found: error 55 | 56 | coverage: 57 | name: Coverage 58 | runs-on: ubuntu-24.04 59 | needs: tests 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - uses: actions/setup-python@v5 64 | with: 65 | python-version: '3.13' 66 | 67 | - name: Install uv 68 | uses: astral-sh/setup-uv@v6 69 | 70 | - name: Install dependencies 71 | run: uv pip install --system coverage[toml] 72 | 73 | - name: Download data 74 | uses: actions/download-artifact@v4 75 | with: 76 | path: ${{ github.workspace }} 77 | pattern: coverage-data-* 78 | merge-multiple: true 79 | 80 | - name: Combine coverage and fail if it's <100% 81 | run: | 82 | python -m coverage combine 83 | python -m coverage html --skip-covered --skip-empty 84 | python -m coverage report --fail-under=100 85 | echo "## Coverage summary" >> $GITHUB_STEP_SUMMARY 86 | python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 87 | 88 | - name: Upload HTML report 89 | if: ${{ failure() }} 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: html-report 93 | path: htmlcov 94 | 95 | release: 96 | needs: [coverage] 97 | if: success() && startsWith(github.ref, 'refs/tags/') 98 | runs-on: ubuntu-24.04 99 | environment: release 100 | 101 | permissions: 102 | contents: read 103 | id-token: write 104 | 105 | steps: 106 | - uses: actions/checkout@v4 107 | 108 | - uses: astral-sh/setup-uv@v6 109 | 110 | - name: Build 111 | run: uv build 112 | 113 | - uses: pypa/gh-action-pypi-publish@release/v1 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | /.coverage 4 | /.coverage.* 5 | /.tox 6 | /build/ 7 | /dist/ 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | 4 | default_language_version: 5 | python: python3.13 6 | 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 10 | hooks: 11 | - id: check-added-large-files 12 | - id: check-case-conflict 13 | - id: check-json 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: check-toml 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | - repo: https://github.com/crate-ci/typos 20 | rev: 6cb49915af2e93e61f5f0d0a82216e28ad5c7c18 # frozen: v1 21 | hooks: 22 | - id: typos 23 | - repo: https://github.com/tox-dev/pyproject-fmt 24 | rev: 57b6ff7bf72affdd12c7f3fd6de761ba18a33b3a # frozen: v2.5.1 25 | hooks: 26 | - id: pyproject-fmt 27 | - repo: https://github.com/tox-dev/tox-ini-fmt 28 | rev: e732f664aa3fd7b32cce3de8abbac43f4c3c375d # frozen: 1.5.0 29 | hooks: 30 | - id: tox-ini-fmt 31 | - repo: https://github.com/rstcheck/rstcheck 32 | rev: f30c4d170a36ea3812bceb5f33004afc213bd797 # frozen: v6.2.4 33 | hooks: 34 | - id: rstcheck 35 | additional_dependencies: 36 | - tomli==2.0.1 37 | - repo: https://github.com/adamchainz/django-upgrade 38 | rev: 700530171ecf380bc829a64388f49d14ecd61c53 # frozen: 1.25.0 39 | hooks: 40 | - id: django-upgrade 41 | args: [--target-version, '4.2'] 42 | - repo: https://github.com/adamchainz/blacken-docs 43 | rev: 78a9dcbecf4f755f65d1f3dec556bc249d723600 # frozen: 1.19.1 44 | hooks: 45 | - id: blacken-docs 46 | additional_dependencies: 47 | - black==25.1.0 48 | - repo: https://github.com/astral-sh/ruff-pre-commit 49 | rev: 12753357c00c3fb8615100354c9fdc6ab80b044d # frozen: v0.11.10 50 | hooks: 51 | - id: ruff-check 52 | args: [ --fix ] 53 | - id: ruff-format 54 | - repo: https://github.com/pre-commit/mirrors-mypy 55 | rev: f40886d54c729f533f864ed6ce584e920feb0af7 # frozen: v1.15.0 56 | hooks: 57 | - id: mypy 58 | additional_dependencies: 59 | - django-stubs==5.1.2 60 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # Configuration file for 'typos' tool 2 | # https://github.com/crate-ci/typos 3 | 4 | [default] 5 | extend-ignore-re = [ 6 | # Single line ignore comments 7 | "(?Rm)^.*(#|//)\\s*typos: ignore$", 8 | # Multi-line ignore comments 9 | "(?s)(#|//)\\s*typos: off.*?\\n\\s*(#|//)\\s*typos: on" 10 | ] 11 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 1.14.0 (2025-02-06) 6 | ------------------- 7 | 8 | * Support Django 5.2. 9 | 10 | 1.13.0 (2024-10-27) 11 | ------------------- 12 | 13 | * Drop Django 3.2 to 4.1 support. 14 | 15 | * Drop Python 3.8 support. 16 | 17 | * Support Python 3.13. 18 | 19 | 1.12.0 (2024-06-19) 20 | ------------------- 21 | 22 | * Support Django 5.1. 23 | 24 | 1.11.0 (2023-10-11) 25 | ------------------- 26 | 27 | * Support Django 5.0. 28 | 29 | 1.10.0 (2023-07-10) 30 | ------------------- 31 | 32 | * Drop Python 3.7 support. 33 | 34 | 1.9.0 (2023-06-14) 35 | ------------------ 36 | 37 | * Support Python 3.12. 38 | 39 | 1.8.0 (2023-02-25) 40 | ------------------ 41 | 42 | * Support Django 4.2. 43 | 44 | 1.7.0 (2022-06-05) 45 | ------------------ 46 | 47 | * Support Python 3.11. 48 | 49 | * Support Django 4.1. 50 | 51 | 1.6.0 (2022-05-10) 52 | ------------------ 53 | 54 | * Drop support for Django 2.2, 3.0, and 3.1. 55 | 56 | 1.5.0 (2022-01-10) 57 | ------------------ 58 | 59 | * Drop Python 3.6 support. 60 | 61 | 1.4.0 (2021-10-05) 62 | ------------------ 63 | 64 | * Support Python 3.10. 65 | 66 | 1.3.0 (2021-09-28) 67 | ------------------ 68 | 69 | * Support Django 4.0. 70 | 71 | 1.2.0 (2021-08-13) 72 | ------------------ 73 | 74 | * Add type hints. 75 | 76 | * Stop distributing tests to reduce package size. Tests are not intended to be 77 | run outside of the tox setup in the repository. Repackagers can use GitHub's 78 | tarballs per tag. 79 | 80 | 1.1.0 (2021-01-25) 81 | ------------------ 82 | 83 | * Support Django 3.2. 84 | 85 | 1.0.3 (2021-01-04) 86 | ------------------ 87 | 88 | * Fix construction of PostgreSQL version from its integer form so that e.g. 89 | ``10.1`` is not parsed as ``10.0.1``. 90 | 91 | `Issue #24 `__. 92 | 93 | 1.0.2 (2020-12-16) 94 | ------------------ 95 | 96 | * Remove dependency on wrapt. 97 | 98 | 1.0.1 (2020-12-14) 99 | ------------------ 100 | 101 | * Fix running on Django 3.1 when ``databases=None``. 102 | 103 | 1.0.0 (2020-12-14) 104 | ------------------ 105 | 106 | * Initial release. 107 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | See https://github.com/adamchainz/django-version-checks/blob/main/CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune tests 2 | include CHANGELOG.rst 3 | include LICENSE 4 | include pyproject.toml 5 | include README.rst 6 | include src/*/py.typed 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | django-version-checks 3 | ===================== 4 | 5 | .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/django-version-checks/main.yml.svg?branch=main&style=for-the-badge 6 | :target: https://github.com/adamchainz/django-version-checks/actions?workflow=CI 7 | 8 | .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge 9 | :target: https://github.com/adamchainz/django-version-checks/actions?workflow=CI 10 | 11 | .. image:: https://img.shields.io/pypi/v/django-version-checks.svg?style=for-the-badge 12 | :target: https://pypi.org/project/django-version-checks/ 13 | 14 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge 15 | :target: https://github.com/psf/black 16 | 17 | .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge 18 | :target: https://github.com/pre-commit/pre-commit 19 | :alt: pre-commit 20 | 21 | System checks for your project's environment. 22 | 23 | ---- 24 | 25 | **Improve your Django and Git skills** with `my books `__. 26 | 27 | ---- 28 | 29 | Requirements 30 | ============ 31 | 32 | Python 3.9 to 3.13 supported. 33 | 34 | Django 4.2 to 5.2 supported. 35 | 36 | Installation 37 | ============ 38 | 39 | First, install with **pip**: 40 | 41 | .. code-block:: bash 42 | 43 | python -m pip install django-version-checks 44 | 45 | Second, add the app to your ``INSTALLED_APPS`` setting: 46 | 47 | .. code-block:: python 48 | 49 | INSTALLED_APPS = [ 50 | ..., 51 | "django_version_checks", 52 | ..., 53 | ] 54 | 55 | Third, add a ``VERSION_CHECKS`` setting with the version checks you want to enforce (as documented below). 56 | For example: 57 | 58 | .. code-block:: python 59 | 60 | VERSION_CHECKS = { 61 | "python": "==3.9.*", 62 | } 63 | 64 | Usage 65 | ===== 66 | 67 | See also the `introductory blog post `__. 68 | 69 | django-version-checks adds several `system checks `__ that can help ensure that the current environment has the right versions of Python, databases, etc. 70 | This is useful when coordinating upgrades across all your infrastructure. 71 | 72 | Note that django-version-checks does not check the versions of your Python dependencies. 73 | This is because mismatched dependency versions are likely to cause ``ImportError``\s or other import-time problems, before system checks run. 74 | To version check your Python dependencies, try `pip-lock `__. 75 | 76 | Checks use the `PEP 440 specifier format `__ via the ``packaging`` module. 77 | This is the same format used by pip, and allows some flexibility in specifying valid version ranges. 78 | The ``~=`` operator is particularly useful. 79 | For example, you can use ``~=3.9.1`` to mean “3.9.1+, but less than 3.10.0”, allowing environments to take on patch releases without changes, but nothing more. 80 | 81 | The individual checks are documented below. 82 | Each occupies a key in the ``VERSION_CHECKS`` dictionary, and documents its supported types for specifiers. 83 | If a check is misconfigured with a bad type or specifier you will see one of these system check errors: 84 | 85 | * ``dvc.E001``: ```` is misconfigured. Expected a ```` but got ````. 86 | * ``dvc.E002``: ```` is misconfigured. ```` is not a valid PEP440 specifier. 87 | 88 | ``mysql`` check 89 | ---------------- 90 | 91 | This check compares the current version of MariaDB/MySQL to the given specifier. 92 | The range can specified either as a single string: 93 | 94 | .. code-block:: python 95 | 96 | VERSION_CHECKS = { 97 | "mysql": "~=10.5.8", 98 | } 99 | 100 | …or as a dictionary mapping database aliases to their specifiers: 101 | 102 | .. code-block:: python 103 | 104 | VERSION_CHECKS = { 105 | "postgresql": { 106 | "default": "~=10.5.8", 107 | "analytics": "~=10.4.17", 108 | }, 109 | } 110 | 111 | Note: as a database check, Django will only run this during ``migrate`` or when using ``check --database`` (Django 3.1+) / ``check --tags database`` (Django <3.1). 112 | See (`docs `__). 113 | 114 | If this check fails, the system check will report: 115 | 116 | * ``dvc.E005``: The current version of MariaDB/MySQL (````) for the ```` database connection does not match the specified range (````). 117 | 118 | ``postgresql`` check 119 | -------------------- 120 | 121 | This check compares the current version of PostgreSQL to the given specifier. 122 | The range can specified either as a single string: 123 | 124 | .. code-block:: python 125 | 126 | VERSION_CHECKS = { 127 | "postgresql": "~=12.2", 128 | } 129 | 130 | …or as a dictionary mapping database aliases to their specifiers: 131 | 132 | .. code-block:: python 133 | 134 | VERSION_CHECKS = { 135 | "postgresql": { 136 | "default": "~=12.2", 137 | "analytics": "~=13.1", 138 | }, 139 | } 140 | 141 | Note: as a database check, Django will only run this during ``migrate`` or when using ``check --database`` (Django 3.1+) / ``check --tags database`` (Django <3.1). 142 | See (`docs `__). 143 | 144 | If this check fails, the system check will report: 145 | 146 | * ``dvc.E004``: The current version of PostgreSQL (````) for the ```` database connection does not match the specified range (````). 147 | 148 | ``python`` check 149 | ---------------- 150 | 151 | This check compares the current version of Python to the given single specifier: 152 | 153 | .. code-block:: python 154 | 155 | VERSION_CHECKS = { 156 | "python": "~=3.9.1", 157 | } 158 | 159 | If this check fails, the system check will report: 160 | 161 | * ``dvc.E003``: The current version of Python (````) does not match the specified range (````). 162 | 163 | ``sqlite`` check 164 | -------------------- 165 | 166 | This check compares the current version of SQLite to the given single specifier: 167 | 168 | .. code-block:: python 169 | 170 | VERSION_CHECKS = { 171 | "sqlite": "~=3.37", 172 | } 173 | 174 | Note: as a database check, Django will only run this during ``migrate`` or when using ``check --database`` (Django 3.1+) / ``check --tags database`` (Django <3.1). 175 | See (`docs `__). 176 | 177 | If this check fails, the system check will report: 178 | 179 | * ``dvc.E006``: The current version of SQLite (````) does not match the specified range (````). 180 | 181 | Example Upgrade 182 | =============== 183 | 184 | Let’s walk through using django-version-checks to upgrade Python from version 3.8 to 3.9. 185 | We have an infrastructure consisting of CI, staging, and production environments, and several developers’ development machines. 186 | 187 | First, we add a pre-existing check to ensure that all environments are on Python 3.8: 188 | 189 | .. code-block:: python 190 | 191 | VERSION_CHECKS = { 192 | "python": "~=3.8.6", 193 | } 194 | 195 | Second, we rewrite the specifier to allow versions of Python 3.9: 196 | 197 | .. code-block:: python 198 | 199 | VERSION_CHECKS = { 200 | "python": ">=3.8.6,<3.10.0", 201 | } 202 | 203 | Third, we upgrade our infrastructure. 204 | We’d probably upgrade in the order: CI, development environments, staging, production. 205 | Each environment should have an automated run of ``manage.py check``, as per the `Django deployment checklist `__. 206 | 207 | Fourth, we change the specifier again to allow Python 3.9 only: 208 | 209 | .. code-block:: python 210 | 211 | VERSION_CHECKS = { 212 | "python": "~=3.9.1", 213 | } 214 | 215 | And we’re upgraded! 🎉 216 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=77", 5 | ] 6 | 7 | [project] 8 | name = "django-version-checks" 9 | version = "1.14.0" 10 | description = "System checks for your project's environment." 11 | readme = "README.rst" 12 | keywords = [ 13 | "Django", 14 | ] 15 | license = "MIT" 16 | license-files = [ "LICENSE" ] 17 | authors = [ 18 | { name = "Adam Johnson", email = "me@adamj.eu" }, 19 | ] 20 | requires-python = ">=3.9" 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Framework :: Django :: 4.2", 24 | "Framework :: Django :: 5.0", 25 | "Framework :: Django :: 5.1", 26 | "Framework :: Django :: 5.2", 27 | "Intended Audience :: Developers", 28 | "Natural Language :: English", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Programming Language :: Python :: 3.13", 36 | "Programming Language :: Python :: Implementation :: CPython", 37 | "Typing :: Typed", 38 | ] 39 | dependencies = [ 40 | "django>=4.2", 41 | "packaging>=20.8", 42 | ] 43 | urls = { Changelog = "https://github.com/adamchainz/django-version-checks/blob/main/CHANGELOG.rst", Funding = "https://adamj.eu/books/", Repository = "https://github.com/adamchainz/django-version-checks" } 44 | 45 | [dependency-groups] 46 | test = [ 47 | "coverage[toml]", 48 | "packaging>=20.8", 49 | "pytest", 50 | "pytest-django", 51 | "pytest-randomly", 52 | ] 53 | django42 = [ "django>=4.2a1,<5; python_version>='3.8'" ] 54 | django50 = [ "django>=5.0a1,<5.1; python_version>='3.10'" ] 55 | django51 = [ "django>=5.1a1,<5.2; python_version>='3.10'" ] 56 | django52 = [ "django>=5.2a1,<6; python_version>='3.10'" ] 57 | 58 | [tool.ruff] 59 | lint.select = [ 60 | # flake8-bugbear 61 | "B", 62 | # flake8-comprehensions 63 | "C4", 64 | # pycodestyle 65 | "E", 66 | # Pyflakes errors 67 | "F", 68 | # isort 69 | "I", 70 | # flake8-simplify 71 | "SIM", 72 | # flake8-tidy-imports 73 | "TID", 74 | # pyupgrade 75 | "UP", 76 | # Pyflakes warnings 77 | "W", 78 | ] 79 | lint.ignore = [ 80 | # flake8-bugbear opinionated rules 81 | "B9", 82 | # line-too-long 83 | "E501", 84 | # suppressible-exception 85 | "SIM105", 86 | # if-else-block-instead-of-if-exp 87 | "SIM108", 88 | ] 89 | lint.extend-safe-fixes = [ 90 | # non-pep585-annotation 91 | "UP006", 92 | ] 93 | lint.isort.required-imports = [ "from __future__ import annotations" ] 94 | 95 | [tool.pyproject-fmt] 96 | max_supported_python = "3.13" 97 | 98 | [tool.pytest.ini_options] 99 | addopts = """\ 100 | --strict-config 101 | --strict-markers 102 | --ds=tests.settings 103 | """ 104 | django_find_project = false 105 | xfail_strict = true 106 | 107 | [tool.coverage.run] 108 | branch = true 109 | parallel = true 110 | source = [ 111 | "django_version_checks", 112 | "tests", 113 | ] 114 | 115 | [tool.coverage.paths] 116 | source = [ 117 | "src", 118 | ".tox/**/site-packages", 119 | ] 120 | 121 | [tool.coverage.report] 122 | show_missing = true 123 | 124 | [tool.mypy] 125 | enable_error_code = [ 126 | "ignore-without-code", 127 | "redundant-expr", 128 | "truthy-bool", 129 | ] 130 | mypy_path = "src/" 131 | namespace_packages = false 132 | strict = true 133 | warn_unreachable = true 134 | 135 | [[tool.mypy.overrides]] 136 | module = "tests.*" 137 | allow_untyped_defs = true 138 | 139 | [tool.rstcheck] 140 | report_level = "ERROR" 141 | 142 | [tool.uv] 143 | conflicts = [ 144 | [ 145 | { group = "django42" }, 146 | { group = "django50" }, 147 | { group = "django51" }, 148 | { group = "django52" }, 149 | ], 150 | ] 151 | -------------------------------------------------------------------------------- /src/django_version_checks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamchainz/django-version-checks/3c04ccd4bfd7d9cd09a0da8bb45598458e70cd29/src/django_version_checks/__init__.py -------------------------------------------------------------------------------- /src/django_version_checks/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.apps import AppConfig 4 | from django.core.checks import Tags, register 5 | 6 | from django_version_checks import checks 7 | 8 | 9 | class DjangoVersionChecksAppConfig(AppConfig): 10 | name = "django_version_checks" 11 | verbose_name = "django-version-checks" 12 | 13 | def ready(self) -> None: 14 | register(Tags.compatibility)(checks.check_config) 15 | register(Tags.compatibility)(checks.check_python_version) 16 | register(Tags.database)(checks.check_postgresql_version) 17 | register(Tags.database)(checks.check_mysql_version) 18 | register(Tags.database)(checks.check_sqlite_version) 19 | -------------------------------------------------------------------------------- /src/django_version_checks/checks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from collections.abc import Generator 5 | from functools import wraps 6 | from typing import Any, Callable, cast 7 | 8 | from django.conf import settings 9 | from django.core.checks import CheckMessage, Error 10 | from django.db import connections 11 | from django.db.backends.base.base import BaseDatabaseWrapper 12 | from packaging.specifiers import InvalidSpecifier, SpecifierSet 13 | from packaging.version import Version 14 | 15 | from django_version_checks.typing import CheckFunc 16 | 17 | 18 | def check_config(**kwargs: Any) -> list[CheckMessage]: 19 | errors: list[CheckMessage] = [] 20 | 21 | if settings.is_overridden("VERSION_CHECKS"): 22 | settings_dict = settings.VERSION_CHECKS 23 | if not isinstance(settings_dict, dict): 24 | errors.append( 25 | bad_type_error( 26 | name="", 27 | expected="dict", 28 | value=settings_dict, 29 | ) 30 | ) 31 | 32 | return errors 33 | 34 | 35 | def get_config() -> dict[str, str | dict[str, str]]: 36 | if not settings.is_overridden("VERSION_CHECKS"): 37 | return {} 38 | if isinstance(settings.VERSION_CHECKS, dict): 39 | return settings.VERSION_CHECKS 40 | return {} 41 | 42 | 43 | def bad_type_error(*, name: str, expected: str, value: object) -> Error: 44 | label = "settings.VERSION_CHECKS" 45 | if name: 46 | label += f"[{name!r}]" 47 | return Error( 48 | id="dvc.E001", 49 | msg=f"{label} is misconfigured. Expected a {expected} but got {value!r}.", 50 | ) 51 | 52 | 53 | def bad_specifier_error(*, name: str, value: object) -> Error: 54 | return Error( 55 | id="dvc.E002", 56 | msg=( 57 | f"settings.VERSION_CHECKS[{name!r}] is misconfigured. {value!r}" 58 | + " is not a valid PEP440 specifier." 59 | ), 60 | ) 61 | 62 | 63 | def parse_specifier_str(*, name: str) -> Callable[[CheckFunc], CheckFunc]: 64 | def decorator(func: CheckFunc) -> CheckFunc: 65 | @wraps(func) 66 | def wrapper(*args: Any, **kwargs: Any) -> list[CheckMessage]: 67 | config = get_config() 68 | if name not in config: 69 | return [] 70 | specifier = config[name] 71 | if not isinstance(specifier, str): 72 | return [bad_type_error(name=name, expected="str", value=specifier)] 73 | 74 | try: 75 | specifier_set = SpecifierSet(specifier) 76 | except InvalidSpecifier: 77 | return [bad_specifier_error(name=name, value=specifier)] 78 | 79 | return func(specifier_set, *args, **kwargs) 80 | 81 | return cast(CheckFunc, wrapper) 82 | 83 | return decorator 84 | 85 | 86 | class AnyDict(dict[str, str]): 87 | def __init__(self, value: str) -> None: 88 | self.value = value 89 | 90 | def __getitem__(self, key: str) -> str: 91 | return self.value 92 | 93 | 94 | def parse_specifier_str_or_dict(*, name: str) -> Callable[[CheckFunc], CheckFunc]: 95 | def decorator(func: CheckFunc) -> CheckFunc: 96 | @wraps(func) 97 | def wrapper(*args: Any, **kwargs: Any) -> list[CheckMessage]: 98 | config = get_config() 99 | if name not in config: 100 | return [] 101 | specifiers = config[name] 102 | 103 | specifier_dict: dict[str, str] 104 | if isinstance(specifiers, str): 105 | try: 106 | specifier_set = SpecifierSet(specifiers) 107 | except InvalidSpecifier: 108 | return [bad_specifier_error(name=name, value=specifiers)] 109 | specifier_dict = AnyDict(specifier_set) 110 | elif ( 111 | isinstance(specifiers, dict) # type: ignore [redundant-expr] 112 | and all(isinstance(a, str) for a in specifiers) 113 | and all(isinstance(s, str) for s in specifiers.values()) 114 | ): 115 | specifier_dict = {} 116 | for alias, specifier in specifiers.items(): 117 | try: 118 | specifier_set = SpecifierSet(specifier) 119 | except InvalidSpecifier: 120 | return [bad_specifier_error(name=name, value=specifier)] 121 | specifier_dict[alias] = specifier_set 122 | else: 123 | return [ 124 | bad_type_error( 125 | name=name, 126 | expected="str or dict[str, str]", 127 | value=specifiers, 128 | ) 129 | ] 130 | 131 | return func(specifier_dict, *args, **kwargs) 132 | 133 | return cast(CheckFunc, wrapper) 134 | 135 | return decorator 136 | 137 | 138 | def db_connections_matching( 139 | databases: list[str] | None, 140 | vendor: str, 141 | ) -> Generator[tuple[str, BaseDatabaseWrapper]]: 142 | if databases is None: 143 | databases_set = set() 144 | else: 145 | databases_set = set(databases) 146 | 147 | for alias in connections: 148 | if alias not in databases_set: 149 | continue 150 | connection = connections[alias] 151 | if connection.vendor != vendor: 152 | continue 153 | yield alias, connection 154 | 155 | 156 | @parse_specifier_str(name="python") 157 | def check_python_version( 158 | specifier_set: SpecifierSet, **kwargs: Any 159 | ) -> list[CheckMessage]: 160 | errors: list[CheckMessage] = [] 161 | 162 | version_string = ( 163 | f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}" 164 | ) 165 | current_version = Version(version_string) 166 | 167 | if current_version not in specifier_set: 168 | errors.append( 169 | Error( 170 | id="dvc.E003", 171 | msg=( 172 | f"The current version of Python ({version_string}) does" 173 | + f" not match the specified range ({specifier_set})." 174 | ), 175 | ) 176 | ) 177 | 178 | return errors 179 | 180 | 181 | @parse_specifier_str_or_dict(name="postgresql") 182 | def check_postgresql_version( 183 | specifier_dict: dict[str, str], 184 | databases: list[str] | None, 185 | **kwargs: Any, 186 | ) -> list[CheckMessage]: 187 | errors: list[CheckMessage] = [] 188 | for alias, connection in db_connections_matching(databases, "postgresql"): 189 | try: 190 | specifier_set = specifier_dict[alias] 191 | except KeyError: 192 | continue 193 | 194 | # See: https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQSERVERVERSION # noqa: E501 195 | pg_version = connection.pg_version # type: ignore [attr-defined] 196 | major = (pg_version // 10_000) % 100 197 | if major < 10: 198 | minor = (pg_version // 100) % 100 199 | patch = pg_version % 100 200 | version_string = f"{major}.{minor}.{patch}" 201 | else: 202 | minor = pg_version % 10_000 203 | version_string = f"{major}.{minor}" 204 | postgresql_version = Version(version_string) 205 | 206 | if postgresql_version not in specifier_set: 207 | errors.append( 208 | Error( 209 | id="dvc.E004", 210 | msg=( 211 | f"The current version of PostgreSQL ({version_string})" 212 | + f" for the {alias} database connection does not match" 213 | + f" the specified range ({specifier_set})." 214 | ), 215 | ) 216 | ) 217 | 218 | return errors 219 | 220 | 221 | @parse_specifier_str_or_dict(name="mysql") 222 | def check_mysql_version( 223 | specifier_dict: dict[str, str], 224 | databases: list[str] | None, 225 | **kwargs: Any, 226 | ) -> list[CheckMessage]: 227 | errors: list[CheckMessage] = [] 228 | for alias, connection in db_connections_matching(databases, "mysql"): 229 | try: 230 | specifier_set = specifier_dict[alias] 231 | except KeyError: 232 | continue 233 | 234 | version_string = ".".join( 235 | str(i) 236 | for i in connection.mysql_version # type: ignore [attr-defined] 237 | ) 238 | mysql_version = Version(version_string) 239 | 240 | if mysql_version not in specifier_set: 241 | errors.append( 242 | Error( 243 | id="dvc.E005", 244 | msg=( 245 | "The current version of MariaDB/MySQL" 246 | + f" ({version_string}) for the {alias} database" 247 | + " connection does not match the specified range" 248 | + f" ({specifier_set})." 249 | ), 250 | ) 251 | ) 252 | 253 | return errors 254 | 255 | 256 | @parse_specifier_str(name="sqlite") 257 | def check_sqlite_version( 258 | specifier_set: SpecifierSet, **kwargs: Any 259 | ) -> list[CheckMessage]: 260 | from sqlite3.dbapi2 import sqlite_version_info 261 | 262 | errors: list[CheckMessage] = [] 263 | 264 | version_string = ".".join(str(i) for i in sqlite_version_info) 265 | sqlite_version = Version(version_string) 266 | 267 | if sqlite_version not in specifier_set: 268 | errors.append( 269 | Error( 270 | id="dvc.E006", 271 | msg=( 272 | f"The current version of SQLite ({version_string}) does" 273 | + f" not match the specified range ({specifier_set})." 274 | ), 275 | ) 276 | ) 277 | 278 | return errors 279 | -------------------------------------------------------------------------------- /src/django_version_checks/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamchainz/django-version-checks/3c04ccd4bfd7d9cd09a0da8bb45598458e70cd29/src/django_version_checks/py.typed -------------------------------------------------------------------------------- /src/django_version_checks/typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Callable 4 | 5 | from django.core.checks import CheckMessage 6 | 7 | CheckFunc = Callable[..., list[CheckMessage]] 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamchainz/django-version-checks/3c04ccd4bfd7d9cd09a0da8bb45598458e70cd29/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | SECRET_KEY = "NOTASECRET" 4 | 5 | DATABASES = { 6 | "default": { 7 | "ENGINE": "django.db.backends.sqlite3", 8 | "NAME": ":memory:", 9 | "ATOMIC_REQUESTS": True, 10 | }, 11 | } 12 | 13 | TIME_ZONE = "UTC" 14 | 15 | INSTALLED_APPS = [ 16 | "django_version_checks", 17 | ] 18 | 19 | USE_TZ = True 20 | -------------------------------------------------------------------------------- /tests/test_checks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import contextmanager 4 | from unittest import mock 5 | 6 | from django.db import connection 7 | from django.test import SimpleTestCase, override_settings 8 | 9 | from django_version_checks import checks 10 | 11 | 12 | class CheckConfigTests(SimpleTestCase): 13 | def test_success_no_setting(self): 14 | errors = checks.check_config() 15 | 16 | assert errors == [] 17 | 18 | @override_settings(VERSION_CHECKS=[]) 19 | def test_fail_bad_type(self): 20 | errors = checks.check_config() 21 | 22 | assert len(errors) == 1 23 | assert errors[0].id == "dvc.E001" 24 | assert ( 25 | errors[0].msg 26 | == "settings.VERSION_CHECKS is misconfigured. Expected a dict but got []." 27 | ) 28 | 29 | @override_settings(VERSION_CHECKS={}) 30 | def test_success_empty_dict(self): 31 | errors = checks.check_config() 32 | 33 | assert errors == [] 34 | 35 | 36 | class GetConfigTests(SimpleTestCase): 37 | def test_no_setting(self): 38 | config = checks.get_config() 39 | 40 | assert config == {} 41 | 42 | @override_settings(VERSION_CHECKS={"something": "here"}) 43 | def test_setting(self): 44 | config = checks.get_config() 45 | 46 | assert config == {"something": "here"} 47 | 48 | @override_settings(VERSION_CHECKS=["woops"]) 49 | def test_bad_setting(self): 50 | config = checks.get_config() 51 | 52 | assert config == {} 53 | 54 | 55 | class CheckPythonVersionTests(SimpleTestCase): 56 | @override_settings(VERSION_CHECKS={"python": 3}) 57 | def test_fail_bad_type(self): 58 | errors = checks.check_python_version() 59 | 60 | assert len(errors) == 1 61 | assert errors[0].id == "dvc.E001" 62 | assert errors[0].msg == ( 63 | "settings.VERSION_CHECKS['python'] is misconfigured. Expected " 64 | + "a str but got 3." 65 | ) 66 | 67 | @override_settings(VERSION_CHECKS={"python": "3"}) 68 | def test_fail_bad_specifier(self): 69 | errors = checks.check_python_version() 70 | 71 | assert len(errors) == 1 72 | assert errors[0].id == "dvc.E002" 73 | assert errors[0].msg == ( 74 | "settings.VERSION_CHECKS['python'] is misconfigured. '3' is " 75 | + "not a valid PEP440 specifier." 76 | ) 77 | 78 | @override_settings(VERSION_CHECKS={"python": "<1.0"}) 79 | def test_fail_out_of_range(self): 80 | errors = checks.check_python_version() 81 | 82 | assert len(errors) == 1 83 | assert errors[0].id == "dvc.E003" 84 | assert errors[0].msg.startswith("The current version of Python ") 85 | assert errors[0].msg.endswith("does not match the specified range (<1.0).") 86 | 87 | @override_settings(VERSION_CHECKS={"python": ">=1.0"}) 88 | def test_success_in_range(self): 89 | errors = checks.check_python_version() 90 | 91 | assert errors == [] 92 | 93 | @override_settings(VERSION_CHECKS={}) 94 | def test_success_unspecified(self): 95 | errors = checks.check_python_version() 96 | 97 | assert errors == [] 98 | 99 | 100 | @contextmanager 101 | def fake_postgresql(*, pg_version): 102 | mock_vendor = mock.patch.object(connection, "vendor", "postgresql") 103 | mock_pg_version = mock.patch.object( 104 | connection, "pg_version", pg_version, create=True 105 | ) 106 | with mock_vendor, mock_pg_version: 107 | yield 108 | 109 | 110 | class CheckPostgresqlVersionTests(SimpleTestCase): 111 | @override_settings(VERSION_CHECKS={"postgresql": 13}) 112 | def test_fail_bad_type(self): 113 | errors = checks.check_postgresql_version(databases=["default"]) 114 | 115 | assert len(errors) == 1 116 | assert errors[0].id == "dvc.E001" 117 | assert errors[0].msg == ( 118 | "settings.VERSION_CHECKS['postgresql'] is misconfigured." 119 | + " Expected a str or dict[str, str] but got 13." 120 | ) 121 | 122 | @override_settings(VERSION_CHECKS={"postgresql": "13.1"}) 123 | def test_fail_bad_specifier(self): 124 | errors = checks.check_postgresql_version(databases=["default"]) 125 | 126 | assert len(errors) == 1 127 | assert errors[0].id == "dvc.E002" 128 | assert errors[0].msg == ( 129 | "settings.VERSION_CHECKS['postgresql'] is misconfigured. '13.1' is " 130 | + "not a valid PEP440 specifier." 131 | ) 132 | 133 | @override_settings(VERSION_CHECKS={"postgresql": {"default": "13.1"}}) 134 | def test_fail_bad_specifier_in_dict(self): 135 | errors = checks.check_postgresql_version(databases=["default"]) 136 | 137 | assert len(errors) == 1 138 | assert errors[0].id == "dvc.E002" 139 | assert errors[0].msg == ( 140 | "settings.VERSION_CHECKS['postgresql'] is misconfigured. '13.1' is " 141 | + "not a valid PEP440 specifier." 142 | ) 143 | 144 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 145 | def test_old_version_out_of_range(self): 146 | with fake_postgresql(pg_version=9_01_05): 147 | errors = checks.check_postgresql_version(databases=["default"]) 148 | 149 | assert len(errors) == 1 150 | assert errors[0].id == "dvc.E004" 151 | assert errors[0].msg == ( 152 | "The current version of PostgreSQL (9.1.5) for the default" 153 | + " database connection does not match the specified range" 154 | + " (~=13.1)." 155 | ) 156 | 157 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 158 | def test_fail_out_of_range(self): 159 | with fake_postgresql(pg_version=13_00_00): 160 | errors = checks.check_postgresql_version(databases=["default"]) 161 | 162 | assert len(errors) == 1 163 | assert errors[0].id == "dvc.E004" 164 | assert errors[0].msg == ( 165 | "The current version of PostgreSQL (13.0) for the default" 166 | + " database connection does not match the specified range" 167 | + " (~=13.1)." 168 | ) 169 | 170 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 171 | def test_success_no_postgresql_connections(self): 172 | errors = checks.check_postgresql_version(databases=["default"]) 173 | 174 | assert errors == [] 175 | 176 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 177 | def test_success_in_range(self): 178 | with fake_postgresql(pg_version=13_02_00): 179 | errors = checks.check_postgresql_version(databases=["default"]) 180 | 181 | assert errors == [] 182 | 183 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 184 | def test_success_not_asked_about(self): 185 | with fake_postgresql(pg_version=13_02_00): 186 | errors = checks.check_postgresql_version(databases=["other"]) 187 | 188 | assert errors == [] 189 | 190 | @override_settings(VERSION_CHECKS={"postgresql": "~=13.1"}) 191 | def test_success_databases_none(self): 192 | with fake_postgresql(pg_version=13_02_00): 193 | errors = checks.check_postgresql_version(databases=None) 194 | 195 | assert errors == [] 196 | 197 | @override_settings(VERSION_CHECKS={"postgresql": {"default": "~=13.1"}}) 198 | def test_success_in_range_specific_alias(self): 199 | with fake_postgresql(pg_version=13_02_00): 200 | errors = checks.check_postgresql_version(databases=["default"]) 201 | 202 | assert errors == [] 203 | 204 | @override_settings(VERSION_CHECKS={"postgresql": {"other": "~=13.1"}}) 205 | def test_success_specified_other_alias(self): 206 | with fake_postgresql(pg_version=13_00_00): 207 | errors = checks.check_postgresql_version(databases=["default"]) 208 | 209 | assert errors == [] 210 | 211 | @override_settings(VERSION_CHECKS={}) 212 | def test_success_unspecified(self): 213 | errors = checks.check_postgresql_version(databases=["default"]) 214 | 215 | assert errors == [] 216 | 217 | 218 | @contextmanager 219 | def fake_mysql(*, mysql_version): 220 | mock_vendor = mock.patch.object(connection, "vendor", "mysql") 221 | mock_mysql_version = mock.patch.object( 222 | connection, "mysql_version", mysql_version, create=True 223 | ) 224 | with mock_vendor, mock_mysql_version: 225 | yield 226 | 227 | 228 | class CheckMysqlVersionTests(SimpleTestCase): 229 | @override_settings(VERSION_CHECKS={"mysql": 10}) 230 | def test_fail_bad_type(self): 231 | errors = checks.check_mysql_version(databases=["default"]) 232 | 233 | assert len(errors) == 1 234 | assert errors[0].id == "dvc.E001" 235 | assert errors[0].msg == ( 236 | "settings.VERSION_CHECKS['mysql'] is misconfigured." 237 | + " Expected a str or dict[str, str] but got 10." 238 | ) 239 | 240 | @override_settings(VERSION_CHECKS={"mysql": "10.5.8"}) 241 | def test_fail_bad_specifier(self): 242 | errors = checks.check_mysql_version(databases=["default"]) 243 | 244 | assert len(errors) == 1 245 | assert errors[0].id == "dvc.E002" 246 | assert errors[0].msg == ( 247 | "settings.VERSION_CHECKS['mysql'] is misconfigured. '10.5.8' is " 248 | + "not a valid PEP440 specifier." 249 | ) 250 | 251 | @override_settings(VERSION_CHECKS={"mysql": {"default": "10.5.8"}}) 252 | def test_fail_bad_specifier_in_dict(self): 253 | errors = checks.check_mysql_version(databases=["default"]) 254 | 255 | assert len(errors) == 1 256 | assert errors[0].id == "dvc.E002" 257 | assert errors[0].msg == ( 258 | "settings.VERSION_CHECKS['mysql'] is misconfigured. '10.5.8' is " 259 | + "not a valid PEP440 specifier." 260 | ) 261 | 262 | @override_settings(VERSION_CHECKS={"mysql": "~=10.5.8"}) 263 | def test_fail_out_of_range(self): 264 | with fake_mysql(mysql_version=(10, 5, 7)): 265 | errors = checks.check_mysql_version(databases=["default"]) 266 | 267 | assert len(errors) == 1 268 | assert errors[0].id == "dvc.E005" 269 | assert errors[0].msg == ( 270 | "The current version of MariaDB/MySQL (10.5.7) for the default" 271 | + " database connection does not match the specified range" 272 | + " (~=10.5.8)." 273 | ) 274 | 275 | @override_settings(VERSION_CHECKS={"mysql": "~=10.5.8"}) 276 | def test_success_no_mysql_connections(self): 277 | errors = checks.check_mysql_version(databases=["default"]) 278 | 279 | assert errors == [] 280 | 281 | @override_settings(VERSION_CHECKS={"mysql": "~=10.5.8"}) 282 | def test_success_in_range(self): 283 | with fake_mysql(mysql_version=(10, 5, 9)): 284 | errors = checks.check_mysql_version(databases=["default"]) 285 | 286 | assert errors == [] 287 | 288 | @override_settings(VERSION_CHECKS={"mysql": "~=10.5.8"}) 289 | def test_success_not_asked_about(self): 290 | with fake_mysql(mysql_version=(10, 5, 7)): 291 | errors = checks.check_mysql_version(databases=["other"]) 292 | 293 | assert errors == [] 294 | 295 | @override_settings(VERSION_CHECKS={"mysql": {"default": "~=10.5.8"}}) 296 | def test_success_in_range_specific_alias(self): 297 | with fake_mysql(mysql_version=(10, 5, 8)): 298 | errors = checks.check_mysql_version(databases=["default"]) 299 | 300 | assert errors == [] 301 | 302 | @override_settings(VERSION_CHECKS={"mysql": {"other": "~=10.5.8"}}) 303 | def test_success_specified_other_alias(self): 304 | with fake_mysql(mysql_version=(10, 5, 7)): 305 | errors = checks.check_mysql_version(databases=["default"]) 306 | 307 | assert errors == [] 308 | 309 | @override_settings(VERSION_CHECKS={}) 310 | def test_success_unspecified(self): 311 | errors = checks.check_mysql_version(databases=["default"]) 312 | 313 | assert errors == [] 314 | 315 | 316 | class CheckSqliteVersionTests(SimpleTestCase): 317 | @override_settings(VERSION_CHECKS={"sqlite": 3}) 318 | def test_fail_bad_type(self): 319 | errors = checks.check_sqlite_version() 320 | 321 | assert len(errors) == 1 322 | assert errors[0].id == "dvc.E001" 323 | assert errors[0].msg == ( 324 | "settings.VERSION_CHECKS['sqlite'] is misconfigured. Expected " 325 | + "a str but got 3." 326 | ) 327 | 328 | @override_settings(VERSION_CHECKS={"sqlite": "3"}) 329 | def test_fail_bad_specifier(self): 330 | errors = checks.check_sqlite_version() 331 | 332 | assert len(errors) == 1 333 | assert errors[0].id == "dvc.E002" 334 | assert errors[0].msg == ( 335 | "settings.VERSION_CHECKS['sqlite'] is misconfigured. '3' is " 336 | + "not a valid PEP440 specifier." 337 | ) 338 | 339 | @override_settings(VERSION_CHECKS={"sqlite": "<1.0"}) 340 | def test_fail_out_of_range(self): 341 | errors = checks.check_sqlite_version() 342 | 343 | assert len(errors) == 1 344 | assert errors[0].id == "dvc.E006" 345 | assert errors[0].msg.startswith("The current version of SQLite ") 346 | assert errors[0].msg.endswith("does not match the specified range (<1.0).") 347 | 348 | @override_settings(VERSION_CHECKS={"sqlite": ">=3.0"}) 349 | def test_success_in_range(self): 350 | errors = checks.check_sqlite_version() 351 | 352 | assert errors == [] 353 | 354 | @override_settings(VERSION_CHECKS={}) 355 | def test_success_unspecified(self): 356 | errors = checks.check_sqlite_version() 357 | 358 | assert errors == [] 359 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | py313-django{52, 51} 6 | py312-django{52, 51, 50, 42} 7 | py311-django{52, 51, 50, 42} 8 | py310-django{52, 51, 50, 42} 9 | py39-django{42} 10 | 11 | [testenv] 12 | runner = uv-venv-lock-runner 13 | package = wheel 14 | wheel_build_env = .pkg 15 | set_env = 16 | PYTHONDEVMODE = 1 17 | commands = 18 | python \ 19 | -W error::ResourceWarning \ 20 | -W error::DeprecationWarning \ 21 | -W error::PendingDeprecationWarning \ 22 | -m coverage run \ 23 | -m pytest {posargs:tests} 24 | dependency_groups = 25 | test 26 | django42: django42 27 | django50: django50 28 | django51: django51 29 | django52: django52 30 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.9" 4 | resolution-markers = [ 5 | "python_full_version >= '3.10'", 6 | "python_full_version < '3.10'", 7 | ] 8 | conflicts = [[ 9 | { package = "django-version-checks", group = "django42" }, 10 | { package = "django-version-checks", group = "django50" }, 11 | { package = "django-version-checks", group = "django51" }, 12 | { package = "django-version-checks", group = "django52" }, 13 | ]] 14 | 15 | [[package]] 16 | name = "asgiref" 17 | version = "3.8.1" 18 | source = { registry = "https://pypi.org/simple" } 19 | dependencies = [ 20 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, 25 | ] 26 | 27 | [[package]] 28 | name = "colorama" 29 | version = "0.4.6" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 34 | ] 35 | 36 | [[package]] 37 | name = "coverage" 38 | version = "7.8.0" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload-time = "2025-03-30T20:34:53.904Z" }, 43 | { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload-time = "2025-03-30T20:34:56.959Z" }, 44 | { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload-time = "2025-03-30T20:34:58.751Z" }, 45 | { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload-time = "2025-03-30T20:35:00.521Z" }, 46 | { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload-time = "2025-03-30T20:35:02.307Z" }, 47 | { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload-time = "2025-03-30T20:35:04.141Z" }, 48 | { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload-time = "2025-03-30T20:35:05.889Z" }, 49 | { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload-time = "2025-03-30T20:35:07.76Z" }, 50 | { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload-time = "2025-03-30T20:35:09.144Z" }, 51 | { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload-time = "2025-03-30T20:35:10.734Z" }, 52 | { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, 53 | { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, 54 | { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, 55 | { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, 56 | { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, 57 | { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, 58 | { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, 59 | { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, 60 | { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, 61 | { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, 62 | { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload-time = "2025-03-30T20:35:29.959Z" }, 63 | { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload-time = "2025-03-30T20:35:31.912Z" }, 64 | { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload-time = "2025-03-30T20:35:33.455Z" }, 65 | { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload-time = "2025-03-30T20:35:35.354Z" }, 66 | { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload-time = "2025-03-30T20:35:37.121Z" }, 67 | { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload-time = "2025-03-30T20:35:39.07Z" }, 68 | { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload-time = "2025-03-30T20:35:40.598Z" }, 69 | { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload-time = "2025-03-30T20:35:42.204Z" }, 70 | { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload-time = "2025-03-30T20:35:44.216Z" }, 71 | { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload-time = "2025-03-30T20:35:45.797Z" }, 72 | { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, 73 | { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, 74 | { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, 75 | { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, 76 | { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, 77 | { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, 78 | { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, 79 | { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, 80 | { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, 81 | { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, 82 | { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, 83 | { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, 84 | { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, 85 | { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, 86 | { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, 87 | { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, 88 | { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, 89 | { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, 90 | { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, 91 | { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, 92 | { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377, upload-time = "2025-03-30T20:36:23.298Z" }, 93 | { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803, upload-time = "2025-03-30T20:36:25.74Z" }, 94 | { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561, upload-time = "2025-03-30T20:36:27.548Z" }, 95 | { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488, upload-time = "2025-03-30T20:36:29.175Z" }, 96 | { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589, upload-time = "2025-03-30T20:36:30.876Z" }, 97 | { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366, upload-time = "2025-03-30T20:36:32.563Z" }, 98 | { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591, upload-time = "2025-03-30T20:36:34.721Z" }, 99 | { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572, upload-time = "2025-03-30T20:36:36.805Z" }, 100 | { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966, upload-time = "2025-03-30T20:36:38.551Z" }, 101 | { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852, upload-time = "2025-03-30T20:36:40.209Z" }, 102 | { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, 103 | { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, 104 | ] 105 | 106 | [package.optional-dependencies] 107 | toml = [ 108 | { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 109 | ] 110 | 111 | [[package]] 112 | name = "django" 113 | version = "4.2.21" 114 | source = { registry = "https://pypi.org/simple" } 115 | resolution-markers = [ 116 | "python_full_version < '3.10'", 117 | "python_full_version >= '3.10'", 118 | ] 119 | dependencies = [ 120 | { name = "asgiref" }, 121 | { name = "sqlparse" }, 122 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 123 | ] 124 | sdist = { url = "https://files.pythonhosted.org/packages/c1/bb/2fad5edc1af2945cb499a2e322ac28e4714fc310bd5201ed1f5a9f73a342/django-4.2.21.tar.gz", hash = "sha256:b54ac28d6aa964fc7c2f7335138a54d78980232011e0cd2231d04eed393dcb0d", size = 10424638, upload-time = "2025-05-07T14:07:07.992Z" } 125 | wheels = [ 126 | { url = "https://files.pythonhosted.org/packages/2c/4f/aeaa3098da18b625ed672f3da6d1cd94e188d1b2cc27c2c841b2f9666282/django-4.2.21-py3-none-any.whl", hash = "sha256:1d658c7bf5d31c7d0cac1cab58bc1f822df89255080fec81909256c30e6180b3", size = 7993839, upload-time = "2025-05-07T14:07:01.318Z" }, 127 | ] 128 | 129 | [[package]] 130 | name = "django" 131 | version = "5.0.14" 132 | source = { registry = "https://pypi.org/simple" } 133 | resolution-markers = [ 134 | "python_full_version >= '3.10'", 135 | ] 136 | dependencies = [ 137 | { name = "asgiref", marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 138 | { name = "sqlparse", marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 139 | { name = "tzdata", marker = "(python_full_version >= '3.10' and sys_platform == 'win32' and extra == 'group-21-django-version-checks-django50') or (python_full_version < '3.10' and extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (python_full_version < '3.10' and extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (python_full_version < '3.10' and extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (sys_platform != 'win32' and extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (sys_platform != 'win32' and extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (sys_platform != 'win32' and extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra != 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 140 | ] 141 | sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/cc0205045386b5be8eecb15a95f290383d103f0db5f7e34f93dcc340d5b0/Django-5.0.14.tar.gz", hash = "sha256:29019a5763dbd48da1720d687c3522ef40d1c61be6fb2fad27ed79e9f655bc11", size = 10644306, upload-time = "2025-04-02T11:24:41.396Z" } 142 | wheels = [ 143 | { url = "https://files.pythonhosted.org/packages/c0/93/eabde8789f41910845567ebbff5aacd52fd80e54c934ce15b83d5f552d2c/Django-5.0.14-py3-none-any.whl", hash = "sha256:e762bef8629ee704de215ebbd32062b84f4e56327eed412e5544f6f6eb1dfd74", size = 8185934, upload-time = "2025-04-02T11:24:36.888Z" }, 144 | ] 145 | 146 | [[package]] 147 | name = "django" 148 | version = "5.1.9" 149 | source = { registry = "https://pypi.org/simple" } 150 | resolution-markers = [ 151 | "python_full_version >= '3.10'", 152 | ] 153 | dependencies = [ 154 | { name = "asgiref", marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52')" }, 155 | { name = "sqlparse", marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52')" }, 156 | { name = "tzdata", marker = "(python_full_version >= '3.10' and sys_platform == 'win32' and extra == 'group-21-django-version-checks-django51') or (python_full_version < '3.10' and extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (sys_platform != 'win32' and extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52')" }, 157 | ] 158 | sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" } 159 | wheels = [ 160 | { url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" }, 161 | ] 162 | 163 | [[package]] 164 | name = "django" 165 | version = "5.2.1" 166 | source = { registry = "https://pypi.org/simple" } 167 | resolution-markers = [ 168 | "python_full_version >= '3.10'", 169 | ] 170 | dependencies = [ 171 | { name = "asgiref", marker = "python_full_version >= '3.10'" }, 172 | { name = "sqlparse", marker = "python_full_version >= '3.10'" }, 173 | { name = "tzdata", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, 174 | ] 175 | sdist = { url = "https://files.pythonhosted.org/packages/ac/10/0d546258772b8f31398e67c85e52c66ebc2b13a647193c3eef8ee433f1a8/django-5.2.1.tar.gz", hash = "sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284", size = 10818735, upload-time = "2025-05-07T14:06:17.543Z" } 176 | wheels = [ 177 | { url = "https://files.pythonhosted.org/packages/90/92/7448697b5838b3a1c6e1d2d6a673e908d0398e84dc4f803a2ce11e7ffc0f/django-5.2.1-py3-none-any.whl", hash = "sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961", size = 8301833, upload-time = "2025-05-07T14:06:10.955Z" }, 178 | ] 179 | 180 | [[package]] 181 | name = "django-version-checks" 182 | version = "1.14.0" 183 | source = { editable = "." } 184 | dependencies = [ 185 | { name = "django", version = "4.2.21", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or extra == 'group-21-django-version-checks-django42' or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 186 | { name = "django", version = "5.0.14", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 187 | { name = "django", version = "5.1.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52')" }, 188 | { name = "django", version = "5.2.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and extra != 'group-21-django-version-checks-django42' and extra != 'group-21-django-version-checks-django50' and extra != 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 189 | { name = "packaging" }, 190 | ] 191 | 192 | [package.dev-dependencies] 193 | django42 = [ 194 | { name = "django", version = "4.2.21", source = { registry = "https://pypi.org/simple" } }, 195 | ] 196 | django50 = [ 197 | { name = "django", version = "5.0.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, 198 | ] 199 | django51 = [ 200 | { name = "django", version = "5.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, 201 | ] 202 | django52 = [ 203 | { name = "django", version = "5.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, 204 | ] 205 | test = [ 206 | { name = "coverage", extra = ["toml"] }, 207 | { name = "packaging" }, 208 | { name = "pytest" }, 209 | { name = "pytest-django" }, 210 | { name = "pytest-randomly" }, 211 | ] 212 | 213 | [package.metadata] 214 | requires-dist = [ 215 | { name = "django", specifier = ">=4.2" }, 216 | { name = "packaging", specifier = ">=20.8" }, 217 | ] 218 | 219 | [package.metadata.requires-dev] 220 | django42 = [{ name = "django", marker = "python_full_version >= '3.8'", specifier = ">=4.2a1,<5" }] 221 | django50 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5.0a1,<5.1" }] 222 | django51 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5.1a1,<5.2" }] 223 | django52 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5.2a1,<6" }] 224 | test = [ 225 | { name = "coverage", extras = ["toml"] }, 226 | { name = "packaging", specifier = ">=20.8" }, 227 | { name = "pytest" }, 228 | { name = "pytest-django" }, 229 | { name = "pytest-randomly" }, 230 | ] 231 | 232 | [[package]] 233 | name = "exceptiongroup" 234 | version = "1.2.2" 235 | source = { registry = "https://pypi.org/simple" } 236 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } 237 | wheels = [ 238 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, 239 | ] 240 | 241 | [[package]] 242 | name = "importlib-metadata" 243 | version = "8.7.0" 244 | source = { registry = "https://pypi.org/simple" } 245 | dependencies = [ 246 | { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 247 | ] 248 | sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } 249 | wheels = [ 250 | { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, 251 | ] 252 | 253 | [[package]] 254 | name = "iniconfig" 255 | version = "2.1.0" 256 | source = { registry = "https://pypi.org/simple" } 257 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 258 | wheels = [ 259 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 260 | ] 261 | 262 | [[package]] 263 | name = "packaging" 264 | version = "25.0" 265 | source = { registry = "https://pypi.org/simple" } 266 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 267 | wheels = [ 268 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 269 | ] 270 | 271 | [[package]] 272 | name = "pluggy" 273 | version = "1.5.0" 274 | source = { registry = "https://pypi.org/simple" } 275 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 276 | wheels = [ 277 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 278 | ] 279 | 280 | [[package]] 281 | name = "pytest" 282 | version = "8.3.5" 283 | source = { registry = "https://pypi.org/simple" } 284 | dependencies = [ 285 | { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 286 | { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 287 | { name = "iniconfig" }, 288 | { name = "packaging" }, 289 | { name = "pluggy" }, 290 | { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 291 | ] 292 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 293 | wheels = [ 294 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 295 | ] 296 | 297 | [[package]] 298 | name = "pytest-django" 299 | version = "4.11.1" 300 | source = { registry = "https://pypi.org/simple" } 301 | dependencies = [ 302 | { name = "pytest" }, 303 | ] 304 | sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } 305 | wheels = [ 306 | { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, 307 | ] 308 | 309 | [[package]] 310 | name = "pytest-randomly" 311 | version = "3.16.0" 312 | source = { registry = "https://pypi.org/simple" } 313 | dependencies = [ 314 | { name = "importlib-metadata", marker = "python_full_version < '3.10' or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django50') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django42' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django51') or (extra == 'group-21-django-version-checks-django50' and extra == 'group-21-django-version-checks-django52') or (extra == 'group-21-django-version-checks-django51' and extra == 'group-21-django-version-checks-django52')" }, 315 | { name = "pytest" }, 316 | ] 317 | sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367, upload-time = "2024-10-25T15:45:34.274Z" } 318 | wheels = [ 319 | { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396, upload-time = "2024-10-25T15:45:32.78Z" }, 320 | ] 321 | 322 | [[package]] 323 | name = "sqlparse" 324 | version = "0.5.3" 325 | source = { registry = "https://pypi.org/simple" } 326 | sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } 327 | wheels = [ 328 | { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, 329 | ] 330 | 331 | [[package]] 332 | name = "tomli" 333 | version = "2.2.1" 334 | source = { registry = "https://pypi.org/simple" } 335 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 336 | wheels = [ 337 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 338 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 339 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 340 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 341 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 342 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 343 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 344 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 345 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 346 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 347 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 348 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 349 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 350 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 351 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 352 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 353 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 354 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 355 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 356 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 357 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 358 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 359 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 360 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 361 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 362 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 363 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 364 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 365 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 366 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 367 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 368 | ] 369 | 370 | [[package]] 371 | name = "typing-extensions" 372 | version = "4.13.2" 373 | source = { registry = "https://pypi.org/simple" } 374 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 375 | wheels = [ 376 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 377 | ] 378 | 379 | [[package]] 380 | name = "tzdata" 381 | version = "2025.2" 382 | source = { registry = "https://pypi.org/simple" } 383 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } 384 | wheels = [ 385 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, 386 | ] 387 | 388 | [[package]] 389 | name = "zipp" 390 | version = "3.21.0" 391 | source = { registry = "https://pypi.org/simple" } 392 | sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" } 393 | wheels = [ 394 | { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" }, 395 | ] 396 | --------------------------------------------------------------------------------