├── tests ├── __init__.py ├── testapp │ ├── __init__.py │ ├── models.py │ └── apps.py ├── settings.py ├── test_apps.py ├── compat.py ├── utils.py ├── test_makemigrations.py ├── test_squashmigrations.py ├── test_checks.py ├── test_create_max_migration_files.py └── test_rebase_migration.py ├── src └── django_linear_migrations │ ├── py.typed │ ├── __init__.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── squashmigrations.py │ │ ├── makemigrations.py │ │ ├── create_max_migration_files.py │ │ └── rebase_migration.py │ └── apps.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── issue.yml ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── dependabot.yml └── workflows │ └── main.yml ├── HISTORY.rst ├── .gitignore ├── MANIFEST.in ├── .editorconfig ├── .typos.toml ├── tox.ini ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── CHANGELOG.rst ├── README.rst └── uv.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/django_linear_migrations/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/django_linear_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | Please report security issues directly over email to me@adamj.eu 2 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | See https://github.com/adamchainz/django-linear-migrations/blob/main/CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | /.coverage 4 | /.coverage.* 5 | /.tox 6 | /build/ 7 | /dist/ 8 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/). 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/testapp/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.db import models 4 | 5 | 6 | class Book(models.Model): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/testapp/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class TestAppConfig(AppConfig): 7 | name = "tests.testapp" 8 | verbose_name = "Test App" 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | "tests.testapp", 17 | "django_linear_migrations", 18 | # Force django_migrations creation by having an app with migrations: 19 | "django.contrib.contenttypes", 20 | ] 21 | 22 | USE_TZ = True 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | py314-django{60, 52} 6 | py313-django{60, 52, 51} 7 | py312-django{60, 52, 51, 50, 42} 8 | py311-django{52, 51, 50, 42} 9 | py310-django{52, 51, 50, 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 | django60: django60 31 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Generator 4 | from contextlib import contextmanager 5 | from typing import Any 6 | 7 | from django.db.migrations.writer import MigrationWriter 8 | 9 | 10 | @contextmanager 11 | def spy_on_migration_writers() -> Generator[dict[str, str]]: 12 | written_migrations = {} 13 | 14 | orig_as_string = MigrationWriter.as_string 15 | 16 | def wrapped_as_string(self: MigrationWriter, *args: Any, **kwargs: Any) -> str: 17 | written_migrations[self.migration.app_label] = self.migration.name 18 | return orig_as_string(self, *args, **kwargs) 19 | 20 | MigrationWriter.as_string = wrapped_as_string # type: ignore [method-assign] 21 | try: 22 | yield written_migrations 23 | finally: 24 | MigrationWriter.as_string = orig_as_string # type: ignore [method-assign] 25 | -------------------------------------------------------------------------------- /.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.14.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/commands/squashmigrations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from django.core.management.commands.squashmigrations import Command as BaseCommand 6 | 7 | from django_linear_migrations.apps import MigrationDetails, first_party_app_configs 8 | from django_linear_migrations.management.commands import spy_on_migration_writers 9 | 10 | 11 | class Command(BaseCommand): 12 | def handle(self, **options: Any) -> None: 13 | with spy_on_migration_writers() as written_migrations: 14 | super().handle(**options) 15 | 16 | first_party_app_labels = { 17 | app_config.label for app_config in first_party_app_configs() 18 | } 19 | 20 | for app_label, migration_name in written_migrations.items(): 21 | if app_label not in first_party_app_labels: 22 | continue 23 | 24 | # A squash migration was generated, update max_migration.txt. 25 | migration_details = MigrationDetails(app_label) 26 | max_migration_txt = migration_details.dir / "max_migration.txt" 27 | max_migration_txt.write_text(f"{migration_name}\n") 28 | -------------------------------------------------------------------------------- /tests/test_apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.apps import apps 4 | from django.test import SimpleTestCase 5 | from django.test.utils import override_settings 6 | 7 | from django_linear_migrations.apps import is_first_party_app_config 8 | 9 | 10 | class IsFirstPartyAppConfigTests(SimpleTestCase): 11 | @override_settings(FIRST_PARTY_APPS=[]) 12 | def test_empty(self): 13 | app_config = apps.get_app_config("testapp") 14 | 15 | assert not is_first_party_app_config(app_config) 16 | 17 | @override_settings(FIRST_PARTY_APPS=["django_linear_migrations"]) 18 | def test_not_named(self): 19 | app_config = apps.get_app_config("testapp") 20 | 21 | assert not is_first_party_app_config(app_config) 22 | 23 | @override_settings(FIRST_PARTY_APPS=["tests.testapp"]) 24 | def test_named_by_path(self): 25 | app_config = apps.get_app_config("testapp") 26 | 27 | assert is_first_party_app_config(app_config) 28 | 29 | @override_settings(FIRST_PARTY_APPS=["tests.testapp.apps.TestAppConfig"]) 30 | def test_named_by_app_config_path(self): 31 | app_config = apps.get_app_config("testapp") 32 | 33 | assert is_first_party_app_config(app_config) 34 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/commands/makemigrations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from django.core.management.commands.makemigrations import Command as BaseCommand 6 | 7 | from django_linear_migrations.apps import MigrationDetails, first_party_app_configs 8 | from django_linear_migrations.management.commands import spy_on_migration_writers 9 | 10 | 11 | class Command(BaseCommand): 12 | def handle(self, *app_labels: Any, **options: Any) -> None: 13 | with spy_on_migration_writers() as written_migrations: 14 | super().handle(*app_labels, **options) 15 | 16 | if options["dry_run"]: 17 | return 18 | 19 | first_party_app_labels = { 20 | app_config.label for app_config in first_party_app_configs() 21 | } 22 | 23 | for app_label, migration_name in written_migrations.items(): 24 | if app_label not in first_party_app_labels: 25 | continue 26 | 27 | # Reload required in case of initial migration 28 | migration_details = MigrationDetails(app_label, do_reload=True) 29 | max_migration_txt = migration_details.dir / "max_migration.txt" 30 | max_migration_txt.write_text(f"{migration_name}\n") 31 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import unittest 5 | from collections.abc import Callable 6 | from contextlib import AbstractContextManager 7 | from typing import Any, TypeVar 8 | 9 | # TestCase.enterContext() backport, source: 10 | # https://adamj.eu/tech/2022/11/14/unittest-context-methods-python-3-11-backports/ 11 | 12 | _T = TypeVar("_T") 13 | 14 | if sys.version_info < (3, 11): 15 | 16 | def _enter_context(cm: Any, addcleanup: Callable[..., None]) -> Any: 17 | # We look up the special methods on the type to match the with 18 | # statement. 19 | cls = type(cm) 20 | try: 21 | enter = cls.__enter__ 22 | exit = cls.__exit__ 23 | except AttributeError: # pragma: no cover 24 | raise TypeError( 25 | f"'{cls.__module__}.{cls.__qualname__}' object does " 26 | f"not support the context manager protocol" 27 | ) from None 28 | result = enter(cm) 29 | addcleanup(exit, cm, None, None, None) 30 | return result 31 | 32 | 33 | class EnterContextMixin(unittest.TestCase): 34 | if sys.version_info < (3, 11): 35 | 36 | def enterContext(self, cm: AbstractContextManager[_T]) -> _T: 37 | result: _T = _enter_context(cm, self.addCleanup) 38 | return result 39 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import tempfile 5 | import time 6 | from contextlib import contextmanager 7 | from io import StringIO 8 | from pathlib import Path 9 | from textwrap import dedent 10 | 11 | from django.core.management import call_command 12 | from django.test import override_settings 13 | 14 | 15 | @contextmanager 16 | def temp_migrations_module(): 17 | with tempfile.TemporaryDirectory() as tmp_dir: 18 | tmp_path = Path(tmp_dir) 19 | 20 | migrations_module_name = "migrations" + str(time.time()).replace(".", "") 21 | migrations_dir = tmp_path / migrations_module_name 22 | 23 | migrations_dir.mkdir() 24 | sys.path.insert(0, str(tmp_path)) 25 | try: 26 | with override_settings( 27 | MIGRATION_MODULES={"testapp": migrations_module_name} 28 | ): 29 | yield migrations_dir 30 | finally: 31 | sys.path.pop(0) 32 | 33 | 34 | def run_command(*args, **kwargs): 35 | out = StringIO() 36 | err = StringIO() 37 | returncode: int | str | None = 0 38 | try: 39 | call_command(*args, stdout=out, stderr=err, **kwargs) 40 | except SystemExit as exc: # pragma: no cover 41 | returncode = exc.code 42 | return out.getvalue(), err.getvalue(), returncode 43 | 44 | 45 | empty_migration = dedent( 46 | """\ 47 | from django.db import migrations 48 | class Migration(migrations.Migration): 49 | pass 50 | """ 51 | ) 52 | -------------------------------------------------------------------------------- /.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: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.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: 802d5794ff9cf7b15610c47eca99cd1ab757d8d4 # frozen: v1 21 | hooks: 22 | - id: typos 23 | - repo: https://github.com/tox-dev/pyproject-fmt 24 | rev: d252a2a7678b47d1f2eea2f6b846ddfdcd012759 # frozen: v2.11.1 25 | hooks: 26 | - id: pyproject-fmt 27 | - repo: https://github.com/tox-dev/tox-ini-fmt 28 | rev: be26ee0d710a48f7c1acc1291d84082036207bd3 # frozen: 1.7.0 29 | hooks: 30 | - id: tox-ini-fmt 31 | - repo: https://github.com/rstcheck/rstcheck 32 | rev: 27258fde1ee7d3b1e6a7bbc58f4c7b1dd0e719e5 # frozen: v6.2.5 33 | hooks: 34 | - id: rstcheck 35 | additional_dependencies: 36 | - tomli==2.0.1 37 | - repo: https://github.com/adamchainz/django-upgrade 38 | rev: 553731fe59437e0bd2cf18b10144116422bed259 # frozen: 1.29.1 39 | hooks: 40 | - id: django-upgrade 41 | - repo: https://github.com/adamchainz/blacken-docs 42 | rev: dda8db18cfc68df532abf33b185ecd12d5b7b326 # frozen: 1.20.0 43 | hooks: 44 | - id: blacken-docs 45 | additional_dependencies: 46 | - black==25.1.0 47 | - repo: https://github.com/astral-sh/ruff-pre-commit 48 | rev: 36243b70e5ce219623c3503f5afba0f8c96fda55 # frozen: v0.14.7 49 | hooks: 50 | - id: ruff-check 51 | args: [ --fix ] 52 | - id: ruff-format 53 | - repo: https://github.com/pre-commit/mirrors-mypy 54 | rev: c2738302f5cf2bfb559c1f210950badb133613ea # frozen: v1.19.0 55 | hooks: 56 | - id: mypy 57 | additional_dependencies: 58 | - django-stubs==5.1.2 59 | -------------------------------------------------------------------------------- /.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.10' 24 | - '3.11' 25 | - '3.12' 26 | - '3.13' 27 | - '3.14' 28 | 29 | steps: 30 | - uses: actions/checkout@v6 31 | 32 | - uses: actions/setup-python@v6 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | allow-prereleases: true 36 | 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@v7 39 | with: 40 | enable-cache: true 41 | 42 | - name: Run tox targets for ${{ matrix.python-version }} 43 | run: uvx --with tox-uv tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) 44 | 45 | - name: Upload coverage data 46 | uses: actions/upload-artifact@v5 47 | with: 48 | name: coverage-data-${{ matrix.python-version }} 49 | path: '${{ github.workspace }}/.coverage.*' 50 | include-hidden-files: true 51 | if-no-files-found: error 52 | 53 | coverage: 54 | name: Coverage 55 | runs-on: ubuntu-24.04 56 | needs: tests 57 | steps: 58 | - uses: actions/checkout@v6 59 | 60 | - uses: actions/setup-python@v6 61 | with: 62 | python-version: '3.13' 63 | 64 | - name: Install uv 65 | uses: astral-sh/setup-uv@v7 66 | 67 | - name: Install dependencies 68 | run: uv pip install --system coverage[toml] 69 | 70 | - name: Download data 71 | uses: actions/download-artifact@v6 72 | with: 73 | path: ${{ github.workspace }} 74 | pattern: coverage-data-* 75 | merge-multiple: true 76 | 77 | - name: Combine coverage and fail if it's <100% 78 | run: | 79 | python -m coverage combine 80 | python -m coverage html --skip-covered --skip-empty 81 | python -m coverage report --fail-under=100 82 | echo "## Coverage summary" >> $GITHUB_STEP_SUMMARY 83 | python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 84 | 85 | - name: Upload HTML report 86 | if: ${{ failure() }} 87 | uses: actions/upload-artifact@v5 88 | with: 89 | name: html-report 90 | path: htmlcov 91 | 92 | release: 93 | needs: [coverage] 94 | if: success() && startsWith(github.ref, 'refs/tags/') 95 | runs-on: ubuntu-24.04 96 | environment: release 97 | 98 | permissions: 99 | contents: read 100 | id-token: write 101 | 102 | steps: 103 | - uses: actions/checkout@v6 104 | 105 | - uses: astral-sh/setup-uv@v7 106 | 107 | - name: Build 108 | run: uv build 109 | 110 | - uses: pypa/gh-action-pypi-publish@release/v1 111 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/commands/create_max_migration_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import sys 5 | from typing import Any 6 | 7 | from django.apps import apps 8 | from django.core.management.commands.makemigrations import Command as BaseCommand 9 | from django.db.migrations.loader import MigrationLoader 10 | 11 | from django_linear_migrations.apps import ( 12 | MigrationDetails, 13 | first_party_app_configs, 14 | get_graph_plan, 15 | ) 16 | 17 | 18 | class Command(BaseCommand): 19 | help = "Generate max_migration.txt files for first-party apps." 20 | 21 | # Checks disabled because the django-linear-migrations' checks would 22 | # prevent us continuing 23 | requires_system_checks: list[str] = [] 24 | 25 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 26 | parser.add_argument( 27 | "args", 28 | metavar="app_label", 29 | nargs="*", 30 | help="Specify the app label(s) to create max migration files for.", 31 | ) 32 | parser.add_argument( 33 | "--dry-run", 34 | action="store_true", 35 | default=False, 36 | help="Actually create the files.", 37 | ) 38 | parser.add_argument( 39 | "--recreate", 40 | action="store_true", 41 | default=False, 42 | help=( 43 | "Recreate existing files. By default only non-existing files" 44 | + " will be created." 45 | ), 46 | ) 47 | 48 | def handle( 49 | self, *app_labels: str, dry_run: bool, recreate: bool, **options: Any 50 | ) -> None: 51 | # Copied check from makemigrations 52 | labels = set(app_labels) 53 | has_bad_labels = False 54 | for app_label in labels: 55 | try: 56 | apps.get_app_config(app_label) 57 | except LookupError as err: 58 | self.stderr.write(str(err)) 59 | has_bad_labels = True 60 | if has_bad_labels: 61 | sys.exit(2) 62 | 63 | any_created = False 64 | migration_loader = MigrationLoader(None, ignore_no_migrations=True) 65 | graph_plan = get_graph_plan(loader=migration_loader, app_labels=labels) 66 | for app_config in first_party_app_configs(): 67 | if labels and app_config.label not in labels: 68 | continue 69 | 70 | migration_details = MigrationDetails(app_config.label) 71 | if not migration_details.has_migrations: 72 | continue 73 | 74 | max_migration_txt = migration_details.dir / "max_migration.txt" 75 | if recreate or not max_migration_txt.exists(): 76 | if not dry_run: 77 | max_migration_name = [ 78 | k[1] for k in graph_plan if k[0] == app_config.label 79 | ][-1] 80 | max_migration_txt.write_text(max_migration_name + "\n") 81 | self.stdout.write( 82 | f"Created max_migration.txt for {app_config.label}." 83 | ) 84 | else: 85 | self.stdout.write( 86 | f"Would create max_migration.txt for {app_config.label}." 87 | ) 88 | any_created = True 89 | 90 | if not any_created: 91 | self.stdout.write("No max_migration.txt files need creating.") 92 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=77", 5 | ] 6 | 7 | [project] 8 | name = "django-linear-migrations" 9 | version = "2.19.0" 10 | description = "Ensure your migrations are linear." 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.10" 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 | "Framework :: Django :: 6.0", 28 | "Intended Audience :: Developers", 29 | "Natural Language :: English", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python :: 3 :: Only", 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 :: 3.14", 37 | "Programming Language :: Python :: Implementation :: CPython", 38 | "Typing :: Typed", 39 | ] 40 | dependencies = [ 41 | "django>=4.2", 42 | ] 43 | urls.Changelog = "https://github.com/adamchainz/django-linear-migrations/blob/main/CHANGELOG.rst" 44 | urls.Funding = "https://adamj.eu/books/" 45 | urls.Repository = "https://github.com/adamchainz/django-linear-migrations" 46 | 47 | [dependency-groups] 48 | test = [ 49 | "black", 50 | "coverage[toml]", 51 | "pytest", 52 | "pytest-django", 53 | "pytest-randomly", 54 | ] 55 | django42 = [ "django>=4.2a1,<5; python_version>='3.8'" ] 56 | django50 = [ "django>=5a1,<5.1; python_version>='3.10'" ] 57 | django51 = [ "django>=5.1a1,<5.2; python_version>='3.10'" ] 58 | django52 = [ "django>=5.2a1,<6; python_version>='3.10'" ] 59 | django60 = [ "django>=6a1,<6.1; python_version>='3.12'" ] 60 | 61 | [tool.uv] 62 | conflicts = [ 63 | [ 64 | { group = "django42" }, 65 | { group = "django50" }, 66 | { group = "django51" }, 67 | { group = "django52" }, 68 | { group = "django60" }, 69 | ], 70 | ] 71 | 72 | [tool.ruff] 73 | lint.select = [ 74 | # flake8-bugbear 75 | "B", 76 | # flake8-comprehensions 77 | "C4", 78 | # pycodestyle 79 | "E", 80 | # Pyflakes errors 81 | "F", 82 | # isort 83 | "I", 84 | # flake8-simplify 85 | "SIM", 86 | # flake8-tidy-imports 87 | "TID", 88 | # pyupgrade 89 | "UP", 90 | # Pyflakes warnings 91 | "W", 92 | ] 93 | lint.ignore = [ 94 | # flake8-bugbear opinionated rules 95 | "B9", 96 | # line-too-long 97 | "E501", 98 | # suppressible-exception 99 | "SIM105", 100 | # if-else-block-instead-of-if-exp 101 | "SIM108", 102 | ] 103 | lint.extend-safe-fixes = [ 104 | # non-pep585-annotation 105 | "UP006", 106 | ] 107 | lint.isort.required-imports = [ "from __future__ import annotations" ] 108 | 109 | [tool.pyproject-fmt] 110 | max_supported_python = "3.14" 111 | 112 | [tool.pytest.ini_options] 113 | addopts = """\ 114 | --strict-config 115 | --strict-markers 116 | --ds=tests.settings 117 | """ 118 | django_find_project = false 119 | xfail_strict = true 120 | 121 | [tool.coverage.run] 122 | branch = true 123 | parallel = true 124 | source = [ 125 | "django_linear_migrations", 126 | "tests", 127 | ] 128 | 129 | [tool.coverage.paths] 130 | source = [ 131 | "src", 132 | ".tox/**/site-packages", 133 | ] 134 | 135 | [tool.coverage.report] 136 | show_missing = true 137 | 138 | [tool.mypy] 139 | enable_error_code = [ 140 | "ignore-without-code", 141 | "redundant-expr", 142 | "truthy-bool", 143 | ] 144 | mypy_path = "src/" 145 | namespace_packages = false 146 | strict = true 147 | warn_unreachable = true 148 | 149 | [[tool.mypy.overrides]] 150 | module = "tests.*" 151 | allow_untyped_defs = true 152 | 153 | [tool.rstcheck] 154 | report_level = "ERROR" 155 | -------------------------------------------------------------------------------- /tests/test_makemigrations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import partial 4 | from textwrap import dedent 5 | 6 | from django.db import models 7 | from django.test import TestCase, override_settings 8 | 9 | from tests.compat import EnterContextMixin 10 | from tests.utils import run_command, temp_migrations_module 11 | 12 | 13 | class MakeMigrationsTests(EnterContextMixin, TestCase): 14 | def setUp(self): 15 | self.migrations_dir = self.enterContext(temp_migrations_module()) 16 | 17 | call_command = staticmethod(partial(run_command, "makemigrations")) 18 | 19 | def test_dry_run(self): 20 | out, err, returncode = self.call_command("--dry-run", "testapp") 21 | 22 | assert returncode == 0 23 | max_migration_txt = self.migrations_dir / "max_migration.txt" 24 | assert not max_migration_txt.exists() 25 | 26 | def test_creates_max_migration_txt(self): 27 | out, err, returncode = self.call_command("testapp") 28 | 29 | assert returncode == 0 30 | max_migration_txt = self.migrations_dir / "max_migration.txt" 31 | assert max_migration_txt.read_text() == "0001_initial\n" 32 | 33 | def test_update(self): 34 | self.call_command("testapp") 35 | max_migration_txt = self.migrations_dir / "max_migration.txt" 36 | assert max_migration_txt.read_text() == "0001_initial\n" 37 | 38 | class TestUpdateModel(models.Model): 39 | class Meta: 40 | app_label = "testapp" 41 | 42 | out, err, returncode = self.call_command("--update", "testapp") 43 | assert returncode == 0 44 | max_migration_txt = self.migrations_dir / "max_migration.txt" 45 | assert max_migration_txt.read_text() == "0001_initial_updated\n" 46 | 47 | def test_creates_max_migration_txt_given_name(self): 48 | out, err, returncode = self.call_command("testapp", "--name", "brand_new") 49 | 50 | assert returncode == 0 51 | max_migration_txt = self.migrations_dir / "max_migration.txt" 52 | assert max_migration_txt.read_text() == "0001_brand_new\n" 53 | 54 | def test_creates_max_migration_txt_second(self): 55 | (self.migrations_dir / "__init__.py").touch() 56 | (self.migrations_dir / "0001_initial.py").write_text( 57 | dedent( 58 | """\ 59 | from django.db import migrations, models 60 | 61 | 62 | class Migration(migrations.Migration): 63 | initial = True 64 | dependencies = [] 65 | operations = [] 66 | """ 67 | ) 68 | ) 69 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 70 | 71 | out, err, returncode = self.call_command("testapp", "--name", "create_book") 72 | 73 | assert returncode == 0 74 | max_migration_txt = self.migrations_dir / "max_migration.txt" 75 | assert max_migration_txt.read_text() == "0002_create_book\n" 76 | 77 | @override_settings(FIRST_PARTY_APPS=[]) 78 | def test_skips_creating_max_migration_txt_for_non_first_party_app(self): 79 | out, err, returncode = self.call_command("testapp") 80 | 81 | assert returncode == 0 82 | max_migration_txt = self.migrations_dir / "max_migration.txt" 83 | assert not max_migration_txt.exists() 84 | 85 | def test_updates_for_a_merge(self): 86 | (self.migrations_dir / "__init__.py").touch() 87 | (self.migrations_dir / "0001_initial.py").write_text( 88 | dedent( 89 | """\ 90 | from django.db import migrations, models 91 | 92 | 93 | class Migration(migrations.Migration): 94 | initial = True 95 | dependencies = [] 96 | operations = [] 97 | """ 98 | ) 99 | ) 100 | (self.migrations_dir / "0002_first_branch.py").write_text( 101 | dedent( 102 | """\ 103 | from django.db import migrations, models 104 | 105 | 106 | class Migration(migrations.Migration): 107 | dependencies = [ 108 | ('testapp', '0001_initial'), 109 | ] 110 | operations = [] 111 | """ 112 | ) 113 | ) 114 | (self.migrations_dir / "0002_second_branch.py").write_text( 115 | dedent( 116 | """\ 117 | from django.db import migrations, models 118 | 119 | 120 | class Migration(migrations.Migration): 121 | dependencies = [ 122 | ('testapp', '0001_initial'), 123 | ] 124 | operations = [] 125 | """ 126 | ) 127 | ) 128 | (self.migrations_dir / "max_migration.txt").write_text( 129 | "0002_second_branch.py\n" 130 | ) 131 | 132 | out, err, returncode = self.call_command("testapp", "--merge", "--no-input") 133 | 134 | assert returncode == 0 135 | max_migration_txt = self.migrations_dir / "max_migration.txt" 136 | assert ( 137 | max_migration_txt.read_text() 138 | == "0003_merge_0002_first_branch_0002_second_branch\n" 139 | ) 140 | -------------------------------------------------------------------------------- /tests/test_squashmigrations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import partial 4 | from textwrap import dedent 5 | 6 | import django 7 | import pytest 8 | from django.core.management import CommandError 9 | from django.test import TestCase, override_settings 10 | 11 | from tests.compat import EnterContextMixin 12 | from tests.utils import run_command, temp_migrations_module 13 | 14 | 15 | class SquashMigrationsTests(EnterContextMixin, TestCase): 16 | def setUp(self): 17 | self.migrations_dir = self.enterContext(temp_migrations_module()) 18 | 19 | call_command = staticmethod(partial(run_command, "squashmigrations")) 20 | 21 | def test_already_squashed_migration(self): 22 | (self.migrations_dir / "__init__.py").touch() 23 | (self.migrations_dir / "0001_already_squashed.py").write_text( 24 | dedent( 25 | """\ 26 | from django.db import migrations, models 27 | 28 | 29 | class Migration(migrations.Migration): 30 | replaces = [ 31 | ('testapp', '0001_initial'), 32 | ('testapp', '0002_second'), 33 | ] 34 | dependencies = [] 35 | operations = [] 36 | """ 37 | ) 38 | ) 39 | (self.migrations_dir / "__init__.py").touch() 40 | (self.migrations_dir / "0002_new_branch.py").write_text( 41 | dedent( 42 | """\ 43 | from django.db import migrations, models 44 | 45 | 46 | class Migration(migrations.Migration): 47 | dependencies = [ 48 | ('testapp', '0001_already_squashed'), 49 | ] 50 | operations = [] 51 | """ 52 | ) 53 | ) 54 | max_migration_txt = self.migrations_dir / "max_migration.txt" 55 | max_migration_txt.write_text("0002_new_branch\n") 56 | 57 | if django.VERSION < (6, 0): 58 | with pytest.raises(CommandError) as excinfo: 59 | self.call_command("testapp", "0002", "--no-input") 60 | 61 | assert excinfo.value.args[0].startswith( 62 | "You cannot squash squashed migrations!" 63 | ) 64 | assert max_migration_txt.read_text() == "0002_new_branch\n" 65 | else: 66 | out, err, returncode = self.call_command("testapp", "0002", "--no-input") 67 | assert returncode == 0 68 | assert max_migration_txt.read_text() == "0001_squashed_0002_new_branch\n" 69 | 70 | def test_success(self): 71 | (self.migrations_dir / "__init__.py").touch() 72 | (self.migrations_dir / "0001_initial.py").write_text( 73 | dedent( 74 | """\ 75 | from django.db import migrations, models 76 | 77 | 78 | class Migration(migrations.Migration): 79 | initial = True 80 | dependencies = [] 81 | operations = [] 82 | """ 83 | ) 84 | ) 85 | (self.migrations_dir / "__init__.py").touch() 86 | (self.migrations_dir / "0002_second.py").write_text( 87 | dedent( 88 | """\ 89 | from django.db import migrations, models 90 | 91 | 92 | class Migration(migrations.Migration): 93 | dependencies = [ 94 | ('testapp', '0001_initial'), 95 | ] 96 | operations = [] 97 | """ 98 | ) 99 | ) 100 | max_migration_txt = self.migrations_dir / "max_migration.txt" 101 | max_migration_txt.write_text("0002_second\n") 102 | 103 | out, err, returncode = self.call_command("testapp", "0002", "--no-input") 104 | 105 | assert returncode == 0 106 | assert max_migration_txt.read_text() == "0001_squashed_0002_second\n" 107 | 108 | @override_settings(FIRST_PARTY_APPS=[]) 109 | def test_skip_non_first_party_app(self): 110 | (self.migrations_dir / "__init__.py").touch() 111 | (self.migrations_dir / "0001_initial.py").write_text( 112 | dedent( 113 | """\ 114 | from django.db import migrations, models 115 | 116 | 117 | class Migration(migrations.Migration): 118 | initial = True 119 | dependencies = [] 120 | operations = [] 121 | """ 122 | ) 123 | ) 124 | (self.migrations_dir / "__init__.py").touch() 125 | (self.migrations_dir / "0002_second.py").write_text( 126 | dedent( 127 | """\ 128 | from django.db import migrations, models 129 | 130 | 131 | class Migration(migrations.Migration): 132 | dependencies = [ 133 | ('testapp', '0001_initial'), 134 | ] 135 | operations = [] 136 | """ 137 | ) 138 | ) 139 | max_migration_txt = self.migrations_dir / "max_migration.txt" 140 | max_migration_txt.write_text("0002_second\n") 141 | 142 | out, err, returncode = self.call_command("testapp", "0002", "--no-input") 143 | 144 | assert returncode == 0 145 | assert max_migration_txt.read_text() == "0002_second\n" 146 | -------------------------------------------------------------------------------- /tests/test_checks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import time 5 | from textwrap import dedent 6 | 7 | import pytest 8 | from django.test import TestCase, override_settings 9 | 10 | from django_linear_migrations.apps import check_max_migration_files 11 | from tests.utils import empty_migration 12 | 13 | 14 | class CheckMaxMigrationFilesTests(TestCase): 15 | @pytest.fixture(autouse=True) 16 | def tmp_path_fixture(self, tmp_path): 17 | migrations_module_name = "migrations" + str(time.time()).replace(".", "") 18 | self.migrations_dir = tmp_path / migrations_module_name 19 | self.migrations_dir.mkdir() 20 | sys.path.insert(0, str(tmp_path)) 21 | try: 22 | with override_settings( 23 | MIGRATION_MODULES={"testapp": migrations_module_name} 24 | ): 25 | yield 26 | finally: 27 | sys.path.pop(0) 28 | 29 | def test_no_migrations_dir(self): 30 | self.migrations_dir.rmdir() 31 | 32 | result = check_max_migration_files() 33 | 34 | assert result == [] 35 | 36 | def test_empty_migrations_dir(self): 37 | result = check_max_migration_files() 38 | 39 | assert result == [] 40 | 41 | def test_non_package(self): 42 | self.migrations_dir.rmdir() 43 | self.migrations_dir.with_suffix(".py").touch() 44 | 45 | result = check_max_migration_files() 46 | 47 | assert result == [] 48 | 49 | def test_skipped_unspecified_app(self): 50 | (self.migrations_dir / "__init__.py").touch() 51 | 52 | result = check_max_migration_files(app_configs=set()) 53 | 54 | assert result == [] 55 | 56 | def test_dlm_E001(self): 57 | (self.migrations_dir / "__init__.py").touch() 58 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 59 | 60 | result = check_max_migration_files() 61 | 62 | assert len(result) == 1 63 | assert result[0].id == "dlm.E001" 64 | assert result[0].msg == "testapp's max_migration.txt does not exist." 65 | 66 | def test_dlm_E002(self): 67 | (self.migrations_dir / "__init__.py").touch() 68 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 69 | (self.migrations_dir / "max_migration.txt").write_text("line1\nline2\n") 70 | 71 | result = check_max_migration_files() 72 | 73 | assert len(result) == 1 74 | assert result[0].id == "dlm.E002" 75 | assert result[0].msg == "testapp's max_migration.txt contains multiple lines." 76 | 77 | def test_dlm_E003(self): 78 | (self.migrations_dir / "__init__.py").touch() 79 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 80 | (self.migrations_dir / "max_migration.txt").write_text("0001_start\n") 81 | 82 | result = check_max_migration_files() 83 | 84 | assert len(result) == 1 85 | assert result[0].id == "dlm.E003" 86 | assert result[0].msg == ( 87 | "testapp's max_migration.txt points to non-existent migration" 88 | + " '0001_start'." 89 | ) 90 | 91 | def test_dlm_E004(self): 92 | (self.migrations_dir / "__init__.py").touch() 93 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 94 | (self.migrations_dir / "0002_updates.py").write_text( 95 | dedent( 96 | """ 97 | from django.db import migrations 98 | class Migration(migrations.Migration): 99 | dependencies = [('testapp', '0001_initial')] 100 | """ 101 | ) 102 | ) 103 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 104 | 105 | result = check_max_migration_files() 106 | 107 | assert len(result) == 1 108 | assert result[0].id == "dlm.E004" 109 | assert result[0].msg == ( 110 | "testapp's max_migration.txt contains '0001_initial', but the" 111 | + " latest migration is '0002_updates'." 112 | ) 113 | 114 | def test_dlm_E005(self): 115 | (self.migrations_dir / "__init__.py").touch() 116 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 117 | (self.migrations_dir / "custom_name.py").write_text( 118 | dedent( 119 | """ 120 | from django.db import migrations 121 | class Migration(migrations.Migration): 122 | dependencies = [('testapp', '0001_initial')] 123 | """ 124 | ) 125 | ) 126 | (self.migrations_dir / "0002_updates.py").write_text( 127 | dedent( 128 | """ 129 | from django.db import migrations 130 | class Migration(migrations.Migration): 131 | dependencies = [('testapp', '0001_initial')] 132 | """ 133 | ) 134 | ) 135 | (self.migrations_dir / "max_migration.txt").write_text("0002_updates\n") 136 | 137 | result = check_max_migration_files() 138 | assert len(result) == 1 139 | assert result[0].id == "dlm.E005" 140 | assert result[0].msg == ( 141 | "Conflicting migrations detected - multiple leaf nodes " 142 | + "detected for these apps:\n" 143 | + "* testapp: 0002_updates, custom_name" 144 | ) 145 | 146 | def test_okay(self): 147 | (self.migrations_dir / "__init__.py").touch() 148 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 149 | (self.migrations_dir / "0002_updates.py").write_text( 150 | dedent( 151 | """ 152 | from django.db import migrations 153 | class Migration(migrations.Migration): 154 | dependencies = [('testapp', '0001_initial')] 155 | """ 156 | ) 157 | ) 158 | (self.migrations_dir / "max_migration.txt").write_text("0002_updates\n") 159 | 160 | result = check_max_migration_files() 161 | 162 | assert result == [] 163 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | * Drop Python 3.9 support. 6 | 7 | 2.19.0 (2025-09-18) 8 | ------------------- 9 | 10 | * Support Django 6.0. 11 | 12 | 2.18.0 (2025-09-08) 13 | ------------------- 14 | 15 | * Support Python 3.14. 16 | 17 | * Support tuples for Migration.dependencies in ``rebase_migration``. 18 | 19 | Thanks to Tom Grainger for the report in `Issue #368 `__. 20 | 21 | 2.17.0 (2025-02-06) 22 | ------------------- 23 | 24 | * Support Django 5.2. 25 | 26 | 2.16.0 (2024-10-29) 27 | ------------------- 28 | 29 | * Drop Django 3.2 to 4.1 support. 30 | 31 | 2.15.0 (2024-10-13) 32 | ------------------- 33 | 34 | * Make ``makemigrations --merge`` update ``max_migration.txt`` files as well. 35 | 36 | Thanks to Gordon Wrigley for the report in `Issue #78 `__. 37 | 38 | 2.14.0 (2024-10-12) 39 | ------------------- 40 | 41 | * Make ``squashmigrations`` update ``max_migration.txt`` files as well. 42 | 43 | Thanks to Gordon Wrigley for the report in `Issue #329 `__. 44 | 45 | * Drop Python 3.8 support. 46 | 47 | * Support Python 3.13. 48 | 49 | 2.13.0 (2024-06-19) 50 | ------------------- 51 | 52 | * Support Django 5.1. 53 | 54 | 2.12.0 (2023-10-11) 55 | ------------------- 56 | 57 | * Support Django 5.0. 58 | 59 | 2.11.0 (2023-07-10) 60 | ------------------- 61 | 62 | * Drop Python 3.7 support. 63 | 64 | 2.10.0 (2023-07-03) 65 | ------------------- 66 | 67 | * Support Django 4.2’s ``--update`` option for ``makemigrations``. 68 | 69 | Thanks to Elliott Omosheye in `PR #270 `__. 70 | 71 | 2.9.0 (2023-06-14) 72 | ------------------ 73 | 74 | * Support Python 3.12. 75 | 76 | 2.8.0 (2023-05-30) 77 | ------------------ 78 | 79 | * Extend ``rebase_migration`` to detect Git in-progress merges and select the correct migration to rebase. 80 | 81 | Thanks to Dmitry Sleptsov in `PR #260 `__. 82 | 83 | 2.7.0 (2023-02-25) 84 | ------------------ 85 | 86 | * Support Django 4.2. 87 | 88 | 2.6.0 (2023-01-03) 89 | ------------------ 90 | 91 | * Use Django’s ``MigrationLoader`` to find the latest migrations. 92 | This also means django-linear-migrations operations detect migration conflicts. 93 | 94 | Thanks to q0w in `PR #208 `__. 95 | 96 | 2.5.1 (2022-07-20) 97 | ------------------ 98 | 99 | * The ``rebase_migration`` command now runs ``black`` on the modified file, if it is found on your ``PATH``. 100 | This copies `Django 4.1’s behaviour `__ in commands that generate and modify migration files. 101 | 102 | 2.5.0 (2022-06-05) 103 | ------------------ 104 | 105 | * Support Python 3.11. 106 | 107 | * Support Django 4.1. 108 | 109 | 2.4.0 (2022-05-10) 110 | ------------------ 111 | 112 | * Drop support for Django 2.2, 3.0, and 3.1. 113 | 114 | 2.3.0 (2022-01-10) 115 | ------------------ 116 | 117 | * Drop Python 3.6 support. 118 | 119 | 2.2.0 (2021-10-05) 120 | ------------------ 121 | 122 | * Support Python 3.10. 123 | 124 | 2.1.0 (2021-09-28) 125 | ------------------ 126 | 127 | * Support Django 4.0. 128 | 129 | 2.0.0 (2021-08-06) 130 | ------------------ 131 | 132 | * Renamed commands from using hyphens to underscores. 133 | This makes them importable and therefore extensible. 134 | The new names are: 135 | 136 | * ``create-max-migration-files`` -> ``create_max_migration_files`` 137 | * ``rebase-migration`` -> ``rebase_migration`` 138 | 139 | * Added ``--recreate`` flag to ``create_max_migration_files``. 140 | 141 | Thanks to Gordon Wrigley for the feature request in `Issue #79 142 | `__. 143 | 144 | * Add type hints. 145 | 146 | 1.6.0 (2021-04-08) 147 | ------------------ 148 | 149 | * Make ``FIRST_PARTY_APPS`` handling match the behaviour of ``INSTALLED_APPS``. 150 | 151 | Thanks to Martin Bächtold for the report in `Pull Request #62 152 | `__. 153 | 154 | * Stop distributing tests to reduce package size. Tests are not intended to be 155 | run outside of the tox setup in the repository. Repackagers can use GitHub's 156 | tarballs per tag. 157 | 158 | 1.5.1 (2021-03-09) 159 | ------------------ 160 | 161 | * Fix ``rebase-migration`` to handle swappable dependencies and other dynamic 162 | constructs in the ``dependencies`` list. 163 | 164 | Thanks to James Singleton for the report in `Issue #52 165 | `__. 166 | 167 | 1.5.0 (2021-01-25) 168 | ------------------ 169 | 170 | * Support Django 3.2. 171 | 172 | 1.4.0 (2021-01-06) 173 | ------------------ 174 | 175 | * Add the ability to define the list of first-party apps, for cases where the 176 | automatic detection does not work. 177 | 178 | 1.3.0 (2020-12-17) 179 | ------------------ 180 | 181 | * Made ``rebase-migration`` abort if the migration to be rebased has been 182 | applied in any local database. 183 | 184 | 1.2.1 (2020-12-15) 185 | ------------------ 186 | 187 | * Handle apps with whose migrations have been disabled by mapping them to 188 | ``None`` in the ``MIGRATION_MODULES`` setting. 189 | 190 | Thanks to Helmut for the report in `Issue #23 191 | `__. 192 | 193 | 1.2.0 (2020-12-14) 194 | ------------------ 195 | 196 | * Made check for whether migrations exist consistent between the system checks 197 | and ``create-max-migration-files``. 198 | 199 | Thanks to @ahumeau for the report in `Issue #20 200 | `__. 201 | 202 | * Also assume modules in ``dist-packages`` are third-party apps. 203 | 204 | Thanks to Serkan Hosca for `Pull Request #21 205 | `__. 206 | 207 | 1.1.0 (2020-12-13) 208 | ------------------ 209 | 210 | * Rename app config class to ``DjangoLinearMigrationsAppConfig``. 211 | 212 | 1.0.2 (2020-12-11) 213 | ------------------ 214 | 215 | * Fix ``create-max-migration-files`` for apps without migrations folders or 216 | files. 217 | 218 | Thanks to Ferran Jovell for the report in `Issue #13 219 | `__. 220 | 221 | 1.0.1 (2020-12-11) 222 | ------------------ 223 | 224 | * Move initial ``max_migration.txt`` file creation into a separate management 225 | command, ``create-max-migration-files``. 226 | 227 | Thanks to Ferran Jovell for the report in `Issue #11 228 | `__. 229 | 230 | 1.0.0 (2020-12-10) 231 | ------------------ 232 | 233 | * Initial release. 234 | -------------------------------------------------------------------------------- /tests/test_create_max_migration_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import time 5 | from io import StringIO 6 | from textwrap import dedent 7 | 8 | import pytest 9 | from django.core.management import call_command 10 | from django.test import TestCase, override_settings 11 | 12 | from tests.utils import empty_migration 13 | 14 | 15 | class CreateMaxMigrationFilesTests(TestCase): 16 | @pytest.fixture(autouse=True) 17 | def tmp_path_fixture(self, tmp_path): 18 | migrations_module_name = "migrations" + str(time.time()).replace(".", "") 19 | self.migrations_dir = tmp_path / migrations_module_name 20 | self.migrations_dir.mkdir() 21 | sys.path.insert(0, str(tmp_path)) 22 | try: 23 | with override_settings( 24 | MIGRATION_MODULES={"testapp": migrations_module_name} 25 | ): 26 | yield 27 | finally: 28 | sys.path.pop(0) 29 | 30 | def call_command(self, *args: str) -> tuple[str, str, int | str | None]: 31 | out = StringIO() 32 | err = StringIO() 33 | returncode: int | str | None = 0 34 | try: 35 | call_command( 36 | "create_max_migration_files", 37 | *args, 38 | stdout=out, 39 | stderr=err, 40 | ) 41 | except SystemExit as exc: 42 | returncode = exc.code 43 | return out.getvalue(), err.getvalue(), returncode 44 | 45 | def test_success_migrations_disabled(self): 46 | self.migrations_dir.rmdir() 47 | with override_settings(MIGRATION_MODULES={"testapp": None}): 48 | out, err, returncode = self.call_command() 49 | 50 | assert out == "No max_migration.txt files need creating.\n" 51 | assert err == "" 52 | assert returncode == 0 53 | 54 | def test_success_no_migrations_dir(self): 55 | self.migrations_dir.rmdir() 56 | 57 | out, err, returncode = self.call_command() 58 | 59 | assert out == "No max_migration.txt files need creating.\n" 60 | assert err == "" 61 | assert returncode == 0 62 | 63 | def test_success_empty_migrations_dir(self): 64 | out, err, returncode = self.call_command() 65 | 66 | assert out == "No max_migration.txt files need creating.\n" 67 | assert err == "" 68 | assert returncode == 0 69 | 70 | def test_success_only_init(self): 71 | (self.migrations_dir / "__init__.py").touch() 72 | 73 | out, err, returncode = self.call_command() 74 | 75 | assert out == "No max_migration.txt files need creating.\n" 76 | assert err == "" 77 | assert returncode == 0 78 | 79 | @override_settings(FIRST_PARTY_APPS=[]) 80 | def test_success_setting_not_first_party(self): 81 | (self.migrations_dir / "__init__.py").touch() 82 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 83 | 84 | out, err, returncode = self.call_command() 85 | 86 | assert out == "No max_migration.txt files need creating.\n" 87 | assert err == "" 88 | assert returncode == 0 89 | 90 | def test_success_dry_run(self): 91 | (self.migrations_dir / "__init__.py").touch() 92 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 93 | 94 | out, err, returncode = self.call_command("--dry-run") 95 | 96 | assert out == "Would create max_migration.txt for testapp.\n" 97 | assert err == "" 98 | assert returncode == 0 99 | max_migration_txt = self.migrations_dir / "max_migration.txt" 100 | assert not max_migration_txt.exists() 101 | 102 | def test_success(self): 103 | (self.migrations_dir / "__init__.py").touch() 104 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 105 | 106 | out, err, returncode = self.call_command() 107 | 108 | assert out == "Created max_migration.txt for testapp.\n" 109 | assert err == "" 110 | assert returncode == 0 111 | max_migration_txt = self.migrations_dir / "max_migration.txt" 112 | assert max_migration_txt.read_text() == "0001_initial\n" 113 | 114 | def test_success_already_exists(self): 115 | (self.migrations_dir / "__init__.py").touch() 116 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 117 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 118 | 119 | out, err, returncode = self.call_command() 120 | 121 | assert out == "No max_migration.txt files need creating.\n" 122 | assert err == "" 123 | assert returncode == 0 124 | 125 | def test_success_recreate(self): 126 | (self.migrations_dir / "__init__.py").touch() 127 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 128 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 129 | 130 | out, err, returncode = self.call_command("--recreate") 131 | 132 | assert out == "Created max_migration.txt for testapp.\n" 133 | assert err == "" 134 | assert returncode == 0 135 | 136 | def test_success_recreate_dry_run(self): 137 | (self.migrations_dir / "__init__.py").touch() 138 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 139 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 140 | 141 | out, err, returncode = self.call_command("--recreate", "--dry-run") 142 | 143 | assert out == "Would create max_migration.txt for testapp.\n" 144 | assert err == "" 145 | assert returncode == 0 146 | 147 | def test_success_specific_app_label(self): 148 | (self.migrations_dir / "__init__.py").touch() 149 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 150 | 151 | out, err, returncode = self.call_command("testapp") 152 | 153 | assert out == "Created max_migration.txt for testapp.\n" 154 | assert err == "" 155 | assert returncode == 0 156 | max_migration_txt = self.migrations_dir / "max_migration.txt" 157 | assert max_migration_txt.read_text() == "0001_initial\n" 158 | 159 | def test_error_specific_bad_app_label(self): 160 | out, err, returncode = self.call_command("badapp") 161 | 162 | assert out == "" 163 | assert err == "No installed app with label 'badapp'.\n" 164 | assert returncode == 2 165 | 166 | def test_success_ignored_app_label(self): 167 | out, err, returncode = self.call_command( 168 | "django_linear_migrations", 169 | ) 170 | 171 | assert out == "No max_migration.txt files need creating.\n" 172 | assert err == "" 173 | assert returncode == 0 174 | 175 | def test_success_custom_migration_name(self): 176 | (self.migrations_dir / "__init__.py").touch() 177 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 178 | (self.migrations_dir / "custom_name.py").write_text( 179 | dedent( 180 | """ 181 | from django.db import migrations 182 | class Migration(migrations.Migration): 183 | dependencies = [('testapp', '0001_initial')] 184 | """ 185 | ) 186 | ) 187 | (self.migrations_dir / "0002_updates.py").write_text( 188 | dedent( 189 | """ 190 | from django.db import migrations 191 | class Migration(migrations.Migration): 192 | dependencies = [('testapp', 'custom_name')] 193 | """ 194 | ) 195 | ) 196 | 197 | out, err, returncode = self.call_command() 198 | 199 | assert out == "Created max_migration.txt for testapp.\n" 200 | assert err == "" 201 | assert returncode == 0 202 | max_migration_txt = self.migrations_dir / "max_migration.txt" 203 | assert max_migration_txt.read_text() == "0002_updates\n" 204 | -------------------------------------------------------------------------------- /src/django_linear_migrations/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pkgutil 4 | from collections.abc import Generator, Iterable 5 | from functools import lru_cache 6 | from importlib import import_module, reload 7 | from pathlib import Path 8 | from types import ModuleType 9 | 10 | from django.apps import AppConfig, apps 11 | from django.conf import settings 12 | from django.core.checks import Error, Tags, register 13 | from django.core.signals import setting_changed 14 | from django.db.migrations.loader import MigrationLoader 15 | from django.dispatch import receiver 16 | from django.utils.functional import cached_property 17 | 18 | 19 | class DjangoLinearMigrationsAppConfig(AppConfig): 20 | name = "django_linear_migrations" 21 | verbose_name = "django-linear-migrations" 22 | 23 | def ready(self) -> None: 24 | register(Tags.models)(check_max_migration_files) 25 | 26 | 27 | @lru_cache(maxsize=1) 28 | def get_first_party_app_labels() -> set[str] | None: 29 | if not settings.is_overridden("FIRST_PARTY_APPS"): 30 | return None 31 | return {AppConfig.create(name).label for name in settings.FIRST_PARTY_APPS} 32 | 33 | 34 | @receiver(setting_changed) 35 | def reset_first_party_app_labels(*, setting: str, **kwargs: object) -> None: 36 | if setting == "FIRST_PARTY_APPS": 37 | get_first_party_app_labels.cache_clear() 38 | 39 | 40 | def is_first_party_app_config(app_config: AppConfig) -> bool: 41 | first_party_labels = get_first_party_app_labels() 42 | if first_party_labels is not None: 43 | return app_config.label in first_party_labels 44 | 45 | # Check if it seems to be installed in a virtualenv 46 | path = Path(app_config.path) 47 | return "site-packages" not in path.parts and "dist-packages" not in path.parts 48 | 49 | 50 | def first_party_app_configs() -> Generator[AppConfig]: 51 | for app_config in apps.get_app_configs(): 52 | if is_first_party_app_config(app_config): 53 | yield app_config 54 | 55 | 56 | class MigrationDetails: 57 | migrations_module_name: str | None 58 | migrations_module: ModuleType | None 59 | 60 | def __init__(self, app_label: str, do_reload: bool = False) -> None: 61 | self.app_label = app_label 62 | 63 | # Some logic duplicated from MigrationLoader.load_disk, but avoiding 64 | # loading all migrations since that's relatively slow. 65 | ( 66 | self.migrations_module_name, 67 | _explicit, 68 | ) = MigrationLoader.migrations_module(app_label) 69 | if self.migrations_module_name is None: 70 | self.migrations_module = None 71 | else: 72 | try: 73 | self.migrations_module = import_module(self.migrations_module_name) 74 | except ModuleNotFoundError: 75 | # Unmigrated app 76 | self.migrations_module = None 77 | else: 78 | if do_reload: 79 | reload(self.migrations_module) 80 | 81 | @property 82 | def has_migrations(self) -> bool: 83 | return ( 84 | self.migrations_module is not None 85 | # Not namespace module: 86 | and self.migrations_module.__file__ is not None 87 | # Django ignores non-package migrations modules 88 | and hasattr(self.migrations_module, "__path__") 89 | and len(self.names) > 0 90 | ) 91 | 92 | @cached_property 93 | def dir(self) -> Path: 94 | assert self.migrations_module is not None 95 | module_file = self.migrations_module.__file__ 96 | assert module_file is not None 97 | return Path(module_file).parent 98 | 99 | @cached_property 100 | def names(self) -> set[str]: 101 | assert self.migrations_module is not None 102 | path = self.migrations_module.__path__ 103 | return { 104 | name 105 | for _, name, is_pkg in pkgutil.iter_modules(path) 106 | if not is_pkg and name[0] not in "_~" 107 | } 108 | 109 | 110 | def get_graph_plan( 111 | loader: MigrationLoader, app_labels: Iterable[str] | None = None 112 | ) -> list[tuple[str, str]]: 113 | nodes = loader.graph.leaf_nodes() 114 | if app_labels: 115 | nodes = [ 116 | (app_label, name) 117 | for app_label, name in loader.graph.leaf_nodes() 118 | if app_label in app_labels 119 | ] 120 | plan: list[tuple[str, str]] = loader.graph._generate_plan(nodes, at_end=True) 121 | return plan 122 | 123 | 124 | def check_max_migration_files( 125 | *, app_configs: Iterable[AppConfig] | None = None, **kwargs: object 126 | ) -> list[Error]: 127 | errors = [] 128 | if app_configs is not None: 129 | app_config_set = set(app_configs) 130 | else: 131 | app_config_set = set() 132 | 133 | migration_loader = MigrationLoader(None, ignore_no_migrations=True) 134 | app_labels = [a.label for a in first_party_app_configs()] 135 | conflicts = { 136 | app_label: names 137 | for app_label, names in migration_loader.detect_conflicts().items() 138 | if app_label in app_labels 139 | } 140 | if conflicts: 141 | conflict_msg = "".join( 142 | f"\n* {app_label}: {', '.join(sorted(names))}" 143 | for app_label, names in conflicts.items() 144 | ) 145 | errors.append( 146 | Error( 147 | id="dlm.E005", 148 | msg=( 149 | "Conflicting migrations detected - multiple leaf nodes " 150 | + f"detected for these apps:{conflict_msg}" 151 | ), 152 | hint=( 153 | "Fix the conflict, e.g. with " 154 | + "'./manage.py makemigrations --merge --skip-checks'." 155 | ), 156 | ) 157 | ) 158 | return errors 159 | 160 | graph_plan = get_graph_plan(loader=migration_loader, app_labels=app_labels) 161 | for app_config in first_party_app_configs(): 162 | # When only checking certain apps, skip the others 163 | if app_configs is not None and app_config not in app_config_set: 164 | continue 165 | app_label = app_config.label 166 | migration_details = MigrationDetails(app_label) 167 | 168 | if not migration_details.has_migrations: 169 | continue 170 | 171 | max_migration_txt = migration_details.dir / "max_migration.txt" 172 | if not max_migration_txt.exists(): 173 | errors.append( 174 | Error( 175 | id="dlm.E001", 176 | msg=f"{app_label}'s max_migration.txt does not exist.", 177 | hint=( 178 | "If you just installed django-linear-migrations, run" 179 | + " 'python manage.py create_max_migration_files'." 180 | + " Otherwise, check how it has gone missing." 181 | ), 182 | ) 183 | ) 184 | continue 185 | 186 | max_migration_txt_lines = max_migration_txt.read_text().strip().splitlines() 187 | if len(max_migration_txt_lines) > 1: 188 | errors.append( 189 | Error( 190 | id="dlm.E002", 191 | msg=f"{app_label}'s max_migration.txt contains multiple lines.", 192 | hint=( 193 | "This may be the result of a git merge. Fix the file" 194 | + " to contain only the name of the latest migration," 195 | + " or maybe use the 'rebase-migration' command." 196 | ), 197 | ) 198 | ) 199 | continue 200 | 201 | max_migration_name = max_migration_txt_lines[0] 202 | if max_migration_name not in migration_details.names: 203 | errors.append( 204 | Error( 205 | id="dlm.E003", 206 | msg=( 207 | f"{app_label}'s max_migration.txt points to" 208 | + f" non-existent migration {max_migration_name!r}." 209 | ), 210 | hint=( 211 | "Edit the max_migration.txt to contain the latest" 212 | + " migration's name." 213 | ), 214 | ) 215 | ) 216 | continue 217 | 218 | real_max_migration_name = [ 219 | name for gp_app_label, name in graph_plan if gp_app_label == app_label 220 | ][-1] 221 | if max_migration_name != real_max_migration_name: 222 | errors.append( 223 | Error( 224 | id="dlm.E004", 225 | msg=( 226 | f"{app_label}'s max_migration.txt contains" 227 | + f" {max_migration_name!r}, but the latest migration" 228 | + f" is {real_max_migration_name!r}." 229 | ), 230 | hint=( 231 | "Edit max_migration.txt to contain" 232 | + f" {real_max_migration_name!r} or rearrange the" 233 | + " migrations into the correct order." 234 | ), 235 | ) 236 | ) 237 | 238 | return errors 239 | -------------------------------------------------------------------------------- /src/django_linear_migrations/management/commands/rebase_migration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import ast 5 | import shutil 6 | import subprocess 7 | from pathlib import Path 8 | from typing import Any 9 | 10 | from django.apps import apps 11 | from django.core.management import BaseCommand, CommandError 12 | from django.db import DatabaseError, connections 13 | from django.db.migrations.recorder import MigrationRecorder 14 | 15 | from django_linear_migrations.apps import MigrationDetails, is_first_party_app_config 16 | 17 | 18 | class Command(BaseCommand): 19 | help = ( 20 | "Fix a conflict in your migration history by rebasing the conflicting" 21 | + " migration on to the end of the app's migration history." 22 | ) 23 | 24 | # Checks disabled because the django-linear-migrations' checks would 25 | # prevent us continuing 26 | requires_system_checks: list[str] = [] 27 | 28 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 29 | parser.add_argument( 30 | "app_label", 31 | help="Specify the app label to rebase the migration for.", 32 | ) 33 | 34 | def handle(self, *args: Any, app_label: str, **options: Any) -> None: 35 | app_config = apps.get_app_config(app_label) 36 | if not is_first_party_app_config(app_config): 37 | raise CommandError(f"{app_label!r} is not a first-party app.") 38 | 39 | migration_details = MigrationDetails(app_label) 40 | max_migration_txt = migration_details.dir / "max_migration.txt" 41 | if not max_migration_txt.exists(): 42 | raise CommandError(f"{app_label} does not have a max_migration.txt.") 43 | 44 | migration_names = find_migration_names( 45 | max_migration_txt.read_text().splitlines() 46 | ) 47 | if migration_names is None: 48 | raise CommandError( 49 | f"{app_label}'s max_migration.txt does not seem to contain a" 50 | + " merge conflict." 51 | ) 52 | merged_migration_name, rebased_migration_name = migration_names 53 | if merged_migration_name not in migration_details.names: 54 | raise CommandError( 55 | f"Parsed {merged_migration_name!r} as the already-merged" 56 | + f" migration name from {app_label}'s max_migration.txt, but" 57 | + " this migration does not exist." 58 | ) 59 | if rebased_migration_name not in migration_details.names: 60 | raise CommandError( 61 | f"Parsed {rebased_migration_name!r} as the rebased migration" 62 | + f" name from {app_label}'s max_migration.txt, but this" 63 | + " migration does not exist." 64 | ) 65 | 66 | rebased_migration_filename = f"{rebased_migration_name}.py" 67 | rebased_migration_path = migration_details.dir / rebased_migration_filename 68 | if not rebased_migration_path.exists(): 69 | raise CommandError( 70 | f"Detected {rebased_migration_filename!r} as the rebased" 71 | + " migration filename, but it does not exist." 72 | ) 73 | 74 | if migration_applied(app_label, rebased_migration_name): 75 | raise CommandError( 76 | f"Detected {rebased_migration_name} as the rebased migration," 77 | + " but it is applied to the local database. Undo the rebase," 78 | + " reverse the migration, and try again." 79 | ) 80 | 81 | content = rebased_migration_path.read_text() 82 | 83 | try: 84 | module_def = ast.parse(content) 85 | except SyntaxError: 86 | raise CommandError( 87 | f"Encountered a SyntaxError trying to parse {rebased_migration_filename!r}." 88 | ) 89 | 90 | # Find the migration class 91 | class_defs = [ 92 | node 93 | for node in module_def.body 94 | if isinstance(node, ast.ClassDef) and node.name == "Migration" 95 | ] 96 | if not class_defs: 97 | raise CommandError( 98 | f"Could not find a Migration class in {rebased_migration_filename!r}." 99 | ) 100 | if len(class_defs) > 1: 101 | raise CommandError( 102 | f"Found multiple Migration classes in {rebased_migration_filename!r}." 103 | ) 104 | migration_class_def = class_defs[0] 105 | 106 | dependencies_assignments = [ 107 | node 108 | for node in migration_class_def.body 109 | if isinstance(node, ast.Assign) 110 | and len(node.targets) == 1 111 | and isinstance(node.targets[0], ast.Name) 112 | and node.targets[0].id == "dependencies" 113 | and isinstance(node.value, (ast.List, ast.Tuple)) 114 | ] 115 | if not dependencies_assignments: 116 | raise CommandError( 117 | f"Could not find a dependencies = [...] assignment in {rebased_migration_filename!r}." 118 | ) 119 | if len(dependencies_assignments) > 1: 120 | raise CommandError( 121 | f"Found multiple dependencies = [...] assignments in {rebased_migration_filename!r}." 122 | ) 123 | 124 | dependencies = dependencies_assignments[0].value 125 | assert isinstance(dependencies, (ast.List, ast.Tuple)) 126 | 127 | lines = content.splitlines(keepends=True) 128 | before_deps_len = ( 129 | sum(len(line) for line in lines[: dependencies.lineno - 1]) 130 | + dependencies.col_offset 131 | ) 132 | assert dependencies.end_lineno is not None 133 | assert dependencies.end_col_offset is not None 134 | after_deps_len = ( 135 | sum(len(line) for line in lines[: dependencies.end_lineno - 1]) 136 | + dependencies.end_col_offset 137 | ) 138 | 139 | before_deps = content[:before_deps_len] 140 | after_deps = content[after_deps_len:] 141 | 142 | if isinstance(dependencies, ast.Tuple): 143 | new_dependencies: ast.Tuple | ast.List = ast.Tuple(elts=[]) 144 | else: 145 | new_dependencies = ast.List(elts=[]) 146 | num_this_app_dependencies = 0 147 | for dependency in dependencies.elts: 148 | # Skip swappable_dependency calls, other dynamically defined 149 | # dependencies, and bad definitions 150 | if ( 151 | not isinstance(dependency, (ast.Tuple, ast.List)) 152 | or len(dependency.elts) != 2 153 | or not all( 154 | isinstance(el, ast.Constant) and isinstance(el.value, str) 155 | for el in dependency.elts 156 | ) 157 | ): 158 | new_dependencies.elts.append(dependency) 159 | continue 160 | 161 | dependency_app_label_node = dependency.elts[0] 162 | assert isinstance(dependency_app_label_node, ast.Constant) 163 | dependency_app_label = dependency_app_label_node.value 164 | assert isinstance(dependency_app_label, str) 165 | 166 | if dependency_app_label == app_label: 167 | num_this_app_dependencies += 1 168 | new_dependencies.elts.append( 169 | ast.Tuple( 170 | elts=[ 171 | ast.Constant(app_label), 172 | ast.Constant(merged_migration_name), 173 | ] 174 | ) 175 | ) 176 | else: 177 | new_dependencies.elts.append(dependency) 178 | 179 | if num_this_app_dependencies != 1: 180 | raise CommandError( 181 | f"Cannot edit {rebased_migration_filename!r} since it has " 182 | + f"{num_this_app_dependencies} dependencies within " 183 | + f"{app_label}." 184 | ) 185 | 186 | new_content = before_deps + ast.unparse(new_dependencies) + after_deps 187 | 188 | merged_number, _merged_rest = merged_migration_name.split("_", 1) 189 | _rebased_number, rebased_rest = rebased_migration_name.split("_", 1) 190 | new_number = int(merged_number) + 1 191 | new_name = str(new_number).zfill(4) + "_" + rebased_rest 192 | new_path_parts = rebased_migration_path.parts[:-1] + (f"{new_name}.py",) 193 | new_path = Path(*new_path_parts) 194 | 195 | rebased_migration_path.rename(new_path) 196 | new_path.write_text(new_content) 197 | max_migration_txt.write_text(f"{new_name}\n") 198 | 199 | black_path = shutil.which("black") 200 | if black_path: # pragma: no cover 201 | subprocess.run( 202 | [black_path, "--fast", "--", new_path], 203 | capture_output=True, 204 | ) 205 | 206 | self.stdout.write( 207 | f"Renamed {rebased_migration_path.parts[-1]} to {new_path.parts[-1]}," 208 | + " updated its dependencies, and updated max_migration.txt." 209 | ) 210 | 211 | 212 | def find_migration_names(max_migration_lines: list[str]) -> tuple[str, str] | None: 213 | lines = max_migration_lines 214 | if len(lines) <= 1: 215 | return None 216 | if not lines[0].startswith("<<<<<<<"): 217 | return None 218 | if not lines[-1].startswith(">>>>>>>"): 219 | return None 220 | migration_names = (lines[1].strip(), lines[-2].strip()) 221 | if is_merge_in_progress(): 222 | # During the merge 'ours' and 'theirs' are swapped in comparison with rebase 223 | migration_names = (migration_names[1], migration_names[0]) 224 | return migration_names 225 | 226 | 227 | def is_merge_in_progress() -> bool: 228 | try: 229 | subprocess.run( 230 | ["git", "rev-parse", "--verify", "MERGE_HEAD"], 231 | capture_output=True, 232 | check=True, 233 | text=True, 234 | ) 235 | except (FileNotFoundError, subprocess.SubprocessError): 236 | # Either: 237 | # - `git` is not available, or broken 238 | # - there is no git repository 239 | # - no merge head exists, so assume rebasing 240 | return False 241 | # Merged head exists, we are merging 242 | return True 243 | 244 | 245 | def migration_applied(app_label: str, migration_name: str) -> bool: 246 | Migration = MigrationRecorder.Migration 247 | for alias in connections: 248 | try: 249 | if ( 250 | Migration.objects.using(alias) 251 | .filter(app=app_label, name=migration_name) 252 | .exists() 253 | ): 254 | return True 255 | except DatabaseError: 256 | # django_migrations table does not exist -> no migrations applied 257 | pass 258 | return False 259 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | django-linear-migrations 3 | ======================== 4 | 5 | .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/django-linear-migrations/main.yml.svg?branch=main&style=for-the-badge 6 | :target: https://github.com/adamchainz/django-linear-migrations/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-linear-migrations/actions?workflow=CI 10 | 11 | .. image:: https://img.shields.io/pypi/v/django-linear-migrations.svg?style=for-the-badge 12 | :target: https://pypi.org/project/django-linear-migrations/ 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 | Ensure your migration history is linear. 22 | 23 | For a bit of background, see the `introductory blog post `__. 24 | 25 | ---- 26 | 27 | **Work smarter and faster** with my book `Boost Your Django DX `__ which covers django-linear-migrations and many other tools to improve your development experience. 28 | 29 | ---- 30 | 31 | Requirements 32 | ============ 33 | 34 | Python 3.10 to 3.14 supported. 35 | 36 | Django 4.2 to 6.0 supported. 37 | 38 | Installation 39 | ============ 40 | 41 | **First,** install with pip: 42 | 43 | .. code-block:: bash 44 | 45 | python -m pip install django-linear-migrations 46 | 47 | **Second,** add the app to your ``INSTALLED_APPS`` setting: 48 | 49 | .. code-block:: python 50 | 51 | INSTALLED_APPS = [ 52 | ..., 53 | "django_linear_migrations", 54 | ..., 55 | ] 56 | 57 | **Third,** check the automatic detection of first-party apps. 58 | Run this command: 59 | 60 | .. code-block:: sh 61 | 62 | python manage.py create_max_migration_files --dry-run 63 | 64 | This command is for creating ``max_migration.txt`` files (more on which later) - in dry run mode it lists the apps it would make such files for. 65 | It tries to automatically detect which apps are first-party, i.e. belong to your project. 66 | The automatic detection checks the path of app’s code to see if is within a virtualenv, but this detection can sometimes fail, for example on editable packages installed with ``-e``. 67 | If you see any apps listed that *aren’t* part of your project, define the list of first-party apps’ labels in a ``FIRST_PARTY_APPS`` setting that you combine into ``INSTALLED_APPS``: 68 | 69 | .. code-block:: python 70 | 71 | FIRST_PARTY_APPS = [] 72 | 73 | INSTALLED_APPS = FIRST_PARTY_APPS + ["django_linear_migrations", ...] 74 | 75 | Note: Django recommends you always list first-party apps first in your project so they can override things in third-party and contrib apps. 76 | 77 | **Fourth,** create the ``max_migration.txt`` files for your first-party apps by re-running the command without the dry run flag: 78 | 79 | .. code-block:: sh 80 | 81 | python manage.py create_max_migration_files 82 | 83 | Usage 84 | ===== 85 | 86 | django-linear-migrations helps you work on Django projects where several branches adding migrations may be in progress at any time. 87 | It enforces that your apps have a *linear* migration history, avoiding merge migrations and the problems they can cause from migrations running in different orders. 88 | It does this by making ``makemigrations`` and ``squashmigrations`` record the name of the latest migration in per-app ``max_migration.txt`` files. 89 | These files will then cause a merge conflicts in your source control tool (Git, Mercurial, etc.) in the case of migrations being developed in parallel. 90 | The first merged migration for an app will prevent the second from being merged, without addressing the conflict. 91 | The included ``rebase_migration`` command can help automatically such conflicts. 92 | 93 | Custom commands 94 | --------------- 95 | 96 | django-linear-migrations relies on overriding the built-in ``makemigrations`` and ``squashmigrations`` commands. 97 | If your project has custom versions of these commands, ensure the app containing your custom commands is **above** ``django_linear_migrations``, and that your commands subclass its ``Command`` class. 98 | For example, for ``makemigrations``: 99 | 100 | .. code-block:: python 101 | 102 | # myapp/management/commands/makemigrations.py 103 | from django_linear_migrations.management.commands.makemigrations import ( 104 | Command as BaseCommand, 105 | ) 106 | 107 | 108 | class Command(BaseCommand): ... 109 | 110 | System Checks 111 | ------------- 112 | 113 | django-linear-migrations comes with several system checks that verify that your ``max_migration.txt`` files are in sync. 114 | These are: 115 | 116 | * ``dlm.E001``: ````'s max_migration.txt does not exist. 117 | * ``dlm.E002``: ````'s max_migration.txt contains multiple lines. 118 | * ``dlm.E003``: ````'s max_migration.txt points to non-existent migration '````'. 119 | * ``dlm.E004``: ````'s max_migration.txt contains '````', but the latest migration is '````'. 120 | * ``dlm.E005``: Conflicting migrations detected; multiple leaf nodes in the migration graph: ```` 121 | 122 | ``create_max_migration_files`` Command 123 | -------------------------------------- 124 | 125 | .. code-block:: sh 126 | 127 | python manage.py create_max_migration_files [app_label [app_label ...]] 128 | 129 | This management command creates ``max_migration.txt`` files for all first party apps, or the given labels. 130 | It’s used in initial installation of django-linear-migrations, and for recreating. 131 | 132 | Pass the ``--dry-run`` flag to only list the ``max_migration.txt`` files that would be created. 133 | 134 | Pass the ``--recreate`` flag to re-create files that already exist. 135 | This may be useful after altering migrations with merges or manually. 136 | 137 | Adding new apps 138 | ^^^^^^^^^^^^^^^ 139 | 140 | When you add a new app to your project, you may need to create its ``max_migration.txt`` file to match any pre-created migrations. 141 | Add the new app to ``INSTALLED_APPS`` or ``FIRST_PARTY_APPS`` as appropriate, then rerun the creation command for the new app by specifying its label: 142 | 143 | .. code-block:: sh 144 | 145 | python manage.py create_max_migration_files my_new_app 146 | 147 | ``rebase_migration`` Command 148 | ---------------------------- 149 | 150 | This management command can help you fix migration conflicts. 151 | Following a conflicted “rebase” operation in Git, run it with the name of the app to auto-fix the migrations for: 152 | 153 | .. code-block:: console 154 | 155 | $ python manage.py rebase_migration 156 | 157 | The command uses the conflict information in the ``max_migration.txt`` file to determine which migration to rebase. 158 | It automatically detects whether a Git merge or rebase operation is in progress, assuming rebase if a Git repository cannot be found. 159 | The command then: 160 | 161 | 1. renames the migration 162 | 2. edits it to depend on the new migration from your main branch 163 | 3. updates ``max_migration.txt``. 164 | 165 | If Black is installed, the command formats the updated migration file with it, like Django’s built-in migration commands do. 166 | See below for some examples and caveats. 167 | 168 | Note rebasing the migration might not always be the *correct* thing to do. 169 | If the migrations in your main and feature branches have both affected the same models, rebasing the migration to the end may not make sense. 170 | However, such parallel changes would *normally* cause conflicts in your model files or other parts of the source code as well. 171 | 172 | Worked Example 173 | ^^^^^^^^^^^^^^ 174 | 175 | Imagine you were working on your project's ``books`` app in a feature branch called ``titles`` and created a migration called ``0002_longer_titles``. 176 | Meanwhile a commit has been merged to your ``main`` branch with a *different* 2nd migration for ``books`` called ``0002_author_nicknames``. 177 | Thanks to django-linear-migrations, the ``max_migration.txt`` file will show as conflicted between your feature and main branches. 178 | 179 | Start the fix by reversing your new migration from your local database. 180 | This is necessary since it will be renamed after rebasing and seen as unapplied. 181 | Do this by switching to the feature branch ``titles`` migrating back to the last common migration: 182 | 183 | .. code-block:: console 184 | 185 | $ git switch titles 186 | $ python manage.py migrate books 0001 187 | 188 | Then, fetch the latest code: 189 | 190 | .. code-block:: console 191 | 192 | $ git switch main 193 | $ git pull 194 | ... 195 | 196 | Next, rebase your ``titles`` branch on top of it. 197 | During this process, Git will detect the conflict on ``max_migration.txt``: 198 | 199 | .. code-block:: console 200 | 201 | $ git switch titles 202 | $ git rebase main 203 | Auto-merging books/models.py 204 | CONFLICT (content): Merge conflict in books/migrations/max_migration.txt 205 | error: could not apply 123456789... Increase Book title length 206 | Resolve all conflicts manually, mark them as resolved with 207 | "git add/rm ", then run "git rebase --continue". 208 | You can instead skip this commit: run "git rebase --skip". 209 | To abort and get back to the state before "git rebase", run "git rebase --abort". 210 | Could not apply 123456789... Increase Book title length 211 | 212 | The contents of the ``books`` app's ``max_migration.txt`` at this point will look something like this: 213 | 214 | .. code-block:: console 215 | 216 | $ cat books/migrations/max_migration.txt 217 | <<<<<<< HEAD 218 | 0002_author_nicknames 219 | ======= 220 | 0002_longer_titles 221 | >>>>>>> 123456789 (Increase Book title length) 222 | 223 | At this point, use ``rebase_migration`` to automatically fix the ``books`` migration history: 224 | 225 | .. code-block:: console 226 | 227 | $ python manage.py rebase_migration books 228 | Renamed 0002_longer_titles.py to 0003_longer_titles.py, updated its dependencies, and updated max_migration.txt. 229 | 230 | This places the conflicted migration on the end of the migration history. 231 | It renames the file appropriately, modifies its ``dependencies = [...]`` declaration, and updates the migration named in ``max_migration.txt`` appropriately. 232 | 233 | After this, you should be able to continue the rebase: 234 | 235 | .. code-block:: console 236 | 237 | $ git add books/migrations 238 | $ git rebase --continue 239 | 240 | And then migrate your local database to allow you to continue development: 241 | 242 | .. code-block:: console 243 | 244 | $ python manage.py migrate books 245 | Operations to perform: 246 | Target specific migration: 0003_longer_titles, from books 247 | Running migrations: 248 | Applying books.0002_author_nicknames... OK 249 | Applying books.0003_longer_titles... OK 250 | 251 | Code Formatting 252 | ^^^^^^^^^^^^^^^ 253 | 254 | ``rebase_migration`` does not guarantee that its edits match your code style. 255 | If you use a formatter like Black, you’ll want to run it after applying ``rebase_migration``. 256 | 257 | If you use `pre-commit `__, note that Git does not invoke hooks during rebase commits. 258 | You can run it manually on changed files with ``pre-commit run``. 259 | 260 | Branches With Multiple Commits 261 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 262 | 263 | Imagine the same example as above, but your feature branch has several commits editing the migration. 264 | This time, before rebasing onto the latest ``main`` branch, squash the commits in your feature branch together. 265 | This way, ``rebase_migration`` can edit the migration file when the conflict occurs. 266 | 267 | You can do this with: 268 | 269 | .. code-block:: console 270 | 271 | $ git rebase -i --keep-base main 272 | 273 | This will open Git’s `interactive mode `__ file. 274 | Edit this so that every commit after the first will be squashed, by starting each line with “s”. 275 | Then close the file, and the rebase will execute. 276 | 277 | After this operation, you can rebase onto your latest ``main`` branch as per the previous example. 278 | 279 | Branches With Multiple Migrations 280 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 281 | 282 | ``rebase_migration`` does not currently support rebasing multiple migrations (in the same app). 283 | This is `an open feature request `__, but it is not a priority, since it’s generally a good idea to restrict changes to one migration at a time. 284 | Consider merging your migrations into one before rebasing. 285 | 286 | Inspiration 287 | =========== 288 | 289 | I’ve seen similar techniques to the one implemented by django-linear-migrations at several places, and they acted as the inspiration for putting this package together. 290 | My previous client `Pollen `__ and current client `ev.energy `__ both have implementations. 291 | This `Doordash blogpost `__ covers a similar system that uses a single file for tracking latest migrations. 292 | And there's also a package called `django-migrations-git-conflicts `__ which works fairly similarly. 293 | -------------------------------------------------------------------------------- /tests/test_rebase_migration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | import time 7 | from functools import partial 8 | from textwrap import dedent 9 | from unittest import mock 10 | 11 | import pytest 12 | from django.core.management import CommandError 13 | from django.db import connection 14 | from django.db.migrations.recorder import MigrationRecorder 15 | from django.test import SimpleTestCase, TestCase, override_settings 16 | 17 | from django_linear_migrations.management.commands import rebase_migration as module 18 | from tests.utils import empty_migration, run_command 19 | 20 | 21 | class RebaseMigrationsTests(TestCase): 22 | @pytest.fixture(autouse=True) 23 | def tmp_path_fixture(self, tmp_path): 24 | migrations_module_name = "migrations" + str(time.time()).replace(".", "") 25 | self.migrations_dir = tmp_path / migrations_module_name 26 | self.migrations_dir.mkdir() 27 | sys.path.insert(0, str(tmp_path)) 28 | try: 29 | with override_settings( 30 | MIGRATION_MODULES={"testapp": migrations_module_name} 31 | ): 32 | yield 33 | finally: 34 | sys.path.pop(0) 35 | 36 | call_command = staticmethod(partial(run_command, "rebase_migration")) 37 | 38 | def test_error_for_non_first_party_app(self): 39 | with ( 40 | mock.patch.object(module, "is_first_party_app_config", return_value=False), 41 | pytest.raises(CommandError) as excinfo, 42 | ): 43 | self.call_command("testapp") 44 | 45 | assert excinfo.value.args[0] == "'testapp' is not a first-party app." 46 | 47 | def test_error_for_no_max_migration_txt(self): 48 | (self.migrations_dir / "__init__.py").touch() 49 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 50 | 51 | with pytest.raises(CommandError) as excinfo: 52 | self.call_command("testapp") 53 | 54 | assert excinfo.value.args[0] == "testapp does not have a max_migration.txt." 55 | 56 | def test_error_for_no_migration_conflict(self): 57 | (self.migrations_dir / "__init__.py").touch() 58 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 59 | (self.migrations_dir / "max_migration.txt").write_text("0001_initial\n") 60 | 61 | with pytest.raises(CommandError) as excinfo: 62 | self.call_command("testapp") 63 | 64 | assert ( 65 | excinfo.value.args[0] 66 | == "testapp's max_migration.txt does not seem to contain a merge conflict." 67 | ) 68 | 69 | def test_error_for_non_existent_merged_migration(self): 70 | (self.migrations_dir / "__init__.py").touch() 71 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 72 | (self.migrations_dir / "max_migration.txt").write_text( 73 | dedent( 74 | """\ 75 | <<<<<<< HEAD 76 | 0002_author_nicknames 77 | ======= 78 | 0002_longer_titles 79 | >>>>>>> 123456789 (Increase Book title length) 80 | """ 81 | ) 82 | ) 83 | 84 | with pytest.raises(CommandError) as excinfo: 85 | self.call_command("testapp") 86 | 87 | assert excinfo.value.args[0] == ( 88 | "Parsed '0002_author_nicknames' as the already-merged migration name" 89 | + " from testapp's max_migration.txt, but this migration does not" 90 | + " exist." 91 | ) 92 | 93 | def test_error_for_non_existent_rebased_migration(self): 94 | (self.migrations_dir / "__init__.py").touch() 95 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 96 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 97 | (self.migrations_dir / "max_migration.txt").write_text( 98 | dedent( 99 | """\ 100 | <<<<<<< HEAD 101 | 0002_author_nicknames 102 | ======= 103 | 0002_longer_titles 104 | >>>>>>> 123456789 (Increase Book title length) 105 | """ 106 | ) 107 | ) 108 | 109 | with pytest.raises(CommandError) as excinfo: 110 | self.call_command("testapp") 111 | 112 | assert excinfo.value.args[0] == ( 113 | "Parsed '0002_longer_titles' as the rebased migration name" 114 | + " from testapp's max_migration.txt, but this migration does not" 115 | + " exist." 116 | ) 117 | 118 | def test_error_for_non_existent_rebased_migration_file(self): 119 | (self.migrations_dir / "__init__.py").touch() 120 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 121 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 122 | (self.migrations_dir / "0002_longer_titles.pyc").write_text(empty_migration) 123 | (self.migrations_dir / "max_migration.txt").write_text( 124 | dedent( 125 | """\ 126 | <<<<<<< HEAD 127 | 0002_author_nicknames 128 | ======= 129 | 0002_longer_titles 130 | >>>>>>> 123456789 (Increase Book title length) 131 | """ 132 | ) 133 | ) 134 | 135 | with pytest.raises(CommandError) as excinfo: 136 | self.call_command("testapp") 137 | 138 | assert excinfo.value.args[0] == ( 139 | "Detected '0002_longer_titles.py' as the rebased migration" 140 | + " filename, but it does not exist." 141 | ) 142 | 143 | def test_error_for_applied_migration(self): 144 | (self.migrations_dir / "__init__.py").touch() 145 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 146 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 147 | (self.migrations_dir / "0002_longer_titles.py").write_text(empty_migration) 148 | (self.migrations_dir / "max_migration.txt").write_text( 149 | dedent( 150 | """\ 151 | <<<<<<< HEAD 152 | 0002_author_nicknames 153 | ======= 154 | 0002_longer_titles 155 | >>>>>>> 123456789 (Increase Book title length) 156 | """ 157 | ) 158 | ) 159 | MigrationRecorder.Migration.objects.create( 160 | app="testapp", name="0002_longer_titles" 161 | ) 162 | 163 | with pytest.raises(CommandError) as excinfo: 164 | self.call_command("testapp") 165 | 166 | assert excinfo.value.args[0] == ( 167 | "Detected 0002_longer_titles as the rebased migration, but it is" 168 | + " applied to the local database. Undo the rebase, reverse the" 169 | + " migration, and try again." 170 | ) 171 | 172 | def test_error_for_unparsable_file(self): 173 | (self.migrations_dir / "__init__.py").touch() 174 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 175 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 176 | (self.migrations_dir / "0002_longer_titles.py").write_text( 177 | dedent( 178 | """\ 179 | from django.db import migrations 180 | 181 | class Migration(migrations.Migration): 182 | dependencies = [(] 183 | operations = [] 184 | """ 185 | ) 186 | ) 187 | (self.migrations_dir / "max_migration.txt").write_text( 188 | dedent( 189 | """\ 190 | <<<<<<< HEAD 191 | 0002_author_nicknames 192 | ======= 193 | 0002_longer_titles 194 | >>>>>>> 123456789 (Increase Book title length) 195 | """ 196 | ) 197 | ) 198 | 199 | with pytest.raises(CommandError) as excinfo: 200 | self.call_command("testapp") 201 | 202 | assert excinfo.value.args[0] == ( 203 | "Encountered a SyntaxError trying to parse '0002_longer_titles.py'." 204 | ) 205 | 206 | def test_error_for_missing_dependencies(self): 207 | (self.migrations_dir / "__init__.py").touch() 208 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 209 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 210 | (self.migrations_dir / "0002_longer_titles.py").write_text( 211 | dedent( 212 | """\ 213 | from django.db import migrations 214 | 215 | class Migration(migrations.Migration): 216 | operations = [] 217 | """ 218 | ) 219 | ) 220 | (self.migrations_dir / "max_migration.txt").write_text( 221 | dedent( 222 | """\ 223 | <<<<<<< HEAD 224 | 0002_author_nicknames 225 | ======= 226 | 0002_longer_titles 227 | >>>>>>> 123456789 (Increase Book title length) 228 | """ 229 | ) 230 | ) 231 | 232 | with pytest.raises(CommandError) as excinfo: 233 | self.call_command("testapp") 234 | 235 | assert excinfo.value.args[0] == ( 236 | "Could not find a dependencies = [...] assignment in '0002_longer_titles.py'." 237 | ) 238 | 239 | def test_error_for_no_migration_class(self): 240 | (self.migrations_dir / "__init__.py").touch() 241 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 242 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 243 | (self.migrations_dir / "0002_longer_titles.py").write_text( 244 | dedent( 245 | """\ 246 | from django.db import migrations 247 | 248 | class MisnamedMigration(migrations.Migration): 249 | dependencies = [] 250 | operations = [] 251 | """ 252 | ) 253 | ) 254 | (self.migrations_dir / "max_migration.txt").write_text( 255 | dedent( 256 | """\ 257 | <<<<<<< HEAD 258 | 0002_author_nicknames 259 | ======= 260 | 0002_longer_titles 261 | >>>>>>> 123456789 (Increase Book title length) 262 | """ 263 | ) 264 | ) 265 | 266 | with pytest.raises(CommandError) as excinfo: 267 | self.call_command("testapp") 268 | 269 | assert excinfo.value.args[0] == ( 270 | "Could not find a Migration class in '0002_longer_titles.py'." 271 | ) 272 | 273 | def test_error_for_multiple_migration_classes(self): 274 | (self.migrations_dir / "__init__.py").touch() 275 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 276 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 277 | (self.migrations_dir / "0002_longer_titles.py").write_text( 278 | dedent( 279 | """\ 280 | from django.db import migrations 281 | 282 | class Migration(migrations.Migration): 283 | dependencies = [] 284 | operations = [] 285 | 286 | class Migration(migrations.Migration): 287 | dependencies = [] 288 | operations = [] 289 | """ 290 | ) 291 | ) 292 | (self.migrations_dir / "max_migration.txt").write_text( 293 | dedent( 294 | """\ 295 | <<<<<<< HEAD 296 | 0002_author_nicknames 297 | ======= 298 | 0002_longer_titles 299 | >>>>>>> 123456789 (Increase Book title length) 300 | """ 301 | ) 302 | ) 303 | 304 | with pytest.raises(CommandError) as excinfo: 305 | self.call_command("testapp") 306 | 307 | assert excinfo.value.args[0] == ( 308 | "Found multiple Migration classes in '0002_longer_titles.py'." 309 | ) 310 | 311 | def test_error_for_multiple_dependencies(self): 312 | (self.migrations_dir / "__init__.py").touch() 313 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 314 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 315 | (self.migrations_dir / "0002_longer_titles.py").write_text( 316 | dedent( 317 | """\ 318 | from django.db import migrations 319 | 320 | class Migration(migrations.Migration): 321 | dependencies = [ 322 | ("otherapp", "0001_initial"), 323 | ] 324 | dependencies = [ 325 | ("otherapp", "0001_initial"), 326 | ] 327 | operations = [] 328 | """ 329 | ) 330 | ) 331 | (self.migrations_dir / "max_migration.txt").write_text( 332 | dedent( 333 | """\ 334 | <<<<<<< HEAD 335 | 0002_author_nicknames 336 | ======= 337 | 0002_longer_titles 338 | >>>>>>> 123456789 (Increase Book title length) 339 | """ 340 | ) 341 | ) 342 | 343 | with pytest.raises(CommandError) as excinfo: 344 | self.call_command("testapp") 345 | 346 | assert excinfo.value.args[0] == ( 347 | "Found multiple dependencies = [...] assignments in '0002_longer_titles.py'." 348 | ) 349 | 350 | def test_error_for_no_dependencies(self): 351 | (self.migrations_dir / "__init__.py").touch() 352 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 353 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 354 | (self.migrations_dir / "0002_longer_titles.py").write_text( 355 | dedent( 356 | """\ 357 | from django.db import migrations 358 | 359 | class Migration(migrations.Migration): 360 | dependencies = [ 361 | ("otherapp", "0001_initial"), 362 | ] 363 | operations = [] 364 | """ 365 | ) 366 | ) 367 | (self.migrations_dir / "max_migration.txt").write_text( 368 | dedent( 369 | """\ 370 | <<<<<<< HEAD 371 | 0002_author_nicknames 372 | ======= 373 | 0002_longer_titles 374 | >>>>>>> 123456789 (Increase Book title length) 375 | """ 376 | ) 377 | ) 378 | 379 | with pytest.raises(CommandError) as excinfo: 380 | self.call_command("testapp") 381 | 382 | assert excinfo.value.args[0] == ( 383 | "Cannot edit '0002_longer_titles.py' since it has 0 dependencies" 384 | + " within testapp." 385 | ) 386 | 387 | def test_error_for_double_dependencies(self): 388 | (self.migrations_dir / "__init__.py").touch() 389 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 390 | (self.migrations_dir / "0002_author_nicknames.py").write_text(empty_migration) 391 | (self.migrations_dir / "0002_longer_titles.py").write_text( 392 | dedent( 393 | """\ 394 | from django.db import migrations 395 | 396 | class Migration(migrations.Migration): 397 | dependencies = [ 398 | ("testapp", "0001_initial"), 399 | ("testapp", "0001_initial"), 400 | ] 401 | operations = [] 402 | """ 403 | ) 404 | ) 405 | (self.migrations_dir / "max_migration.txt").write_text( 406 | dedent( 407 | """\ 408 | <<<<<<< HEAD 409 | 0002_author_nicknames 410 | ======= 411 | 0002_longer_titles 412 | >>>>>>> 123456789 (Increase Book title length) 413 | """ 414 | ) 415 | ) 416 | 417 | with pytest.raises(CommandError) as excinfo: 418 | self.call_command("testapp") 419 | 420 | assert excinfo.value.args[0] == ( 421 | "Cannot edit '0002_longer_titles.py' since it has 2 dependencies" 422 | + " within testapp." 423 | ) 424 | 425 | def test_success(self): 426 | (self.migrations_dir / "__init__.py").touch() 427 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 428 | (self.migrations_dir / "0002_longer_titles.py").write_text( 429 | dedent( 430 | """\ 431 | from django.db import migrations 432 | 433 | class Migration(migrations.Migration): 434 | dependencies = [ 435 | ('testapp', '0001_initial'), 436 | ('otherapp', '0001_initial'), 437 | ] 438 | operations = [] 439 | """ 440 | ) 441 | ) 442 | (self.migrations_dir / "0002_author_nicknames.py").touch() 443 | max_migration_txt = self.migrations_dir / "max_migration.txt" 444 | max_migration_txt.write_text( 445 | dedent( 446 | """\ 447 | <<<<<<< HEAD 448 | 0002_author_nicknames 449 | ======= 450 | 0002_longer_titles 451 | >>>>>>> 123456789 (Increase Book title length) 452 | """ 453 | ) 454 | ) 455 | 456 | out, err, returncode = self.call_command("testapp") 457 | 458 | assert out == ( 459 | "Renamed 0002_longer_titles.py to 0003_longer_titles.py," 460 | + " updated its dependencies, and updated max_migration.txt.\n" 461 | ) 462 | assert err == "" 463 | assert returncode == 0 464 | max_migration_txt = self.migrations_dir / "max_migration.txt" 465 | assert max_migration_txt.read_text() == "0003_longer_titles\n" 466 | 467 | assert not (self.migrations_dir / "0002_longer_titles.py").exists() 468 | new_content = (self.migrations_dir / "0003_longer_titles.py").read_text() 469 | deps = '[("testapp", "0002_author_nicknames"), ("otherapp", "0001_initial")]' 470 | assert new_content == dedent( 471 | f"""\ 472 | from django.db import migrations 473 | 474 | 475 | class Migration(migrations.Migration): 476 | dependencies = {deps} 477 | operations = [] 478 | """ 479 | ) 480 | 481 | def test_success_dependencies_tuple(self): 482 | (self.migrations_dir / "__init__.py").touch() 483 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 484 | (self.migrations_dir / "0002_longer_titles.py").write_text( 485 | dedent( 486 | """\ 487 | from django.db import migrations 488 | 489 | class Migration(migrations.Migration): 490 | dependencies = ( 491 | ('testapp', '0001_initial'), 492 | ('otherapp', '0001_initial'), 493 | ) 494 | operations = [] 495 | """ 496 | ) 497 | ) 498 | (self.migrations_dir / "0002_author_nicknames.py").touch() 499 | max_migration_txt = self.migrations_dir / "max_migration.txt" 500 | max_migration_txt.write_text( 501 | dedent( 502 | """\ 503 | <<<<<<< HEAD 504 | 0002_author_nicknames 505 | ======= 506 | 0002_longer_titles 507 | >>>>>>> 123456789 (Increase Book title length) 508 | """ 509 | ) 510 | ) 511 | 512 | out, err, returncode = self.call_command("testapp") 513 | 514 | assert out == ( 515 | "Renamed 0002_longer_titles.py to 0003_longer_titles.py," 516 | + " updated its dependencies, and updated max_migration.txt.\n" 517 | ) 518 | assert err == "" 519 | assert returncode == 0 520 | max_migration_txt = self.migrations_dir / "max_migration.txt" 521 | assert max_migration_txt.read_text() == "0003_longer_titles\n" 522 | 523 | assert not (self.migrations_dir / "0002_longer_titles.py").exists() 524 | new_content = (self.migrations_dir / "0003_longer_titles.py").read_text() 525 | deps = '(("testapp", "0002_author_nicknames"), ("otherapp", "0001_initial"))' 526 | assert new_content == dedent( 527 | f"""\ 528 | from django.db import migrations 529 | 530 | 531 | class Migration(migrations.Migration): 532 | dependencies = {deps} 533 | operations = [] 534 | """ 535 | ) 536 | 537 | def test_success_swappable_dependency(self): 538 | (self.migrations_dir / "__init__.py").touch() 539 | (self.migrations_dir / "0001_initial.py").write_text(empty_migration) 540 | (self.migrations_dir / "0002_longer_titles.py").write_text( 541 | dedent( 542 | """\ 543 | from django.db import migrations 544 | 545 | class Migration(migrations.Migration): 546 | dependencies = [ 547 | ('testapp', '0001_initial'), 548 | migrations.swappable_dependency('otherapp.0001_initial'), 549 | ] 550 | operations = [] 551 | """ 552 | ) 553 | ) 554 | (self.migrations_dir / "0002_author_nicknames.py").touch() 555 | max_migration_txt = self.migrations_dir / "max_migration.txt" 556 | max_migration_txt.write_text( 557 | dedent( 558 | """\ 559 | <<<<<<< HEAD 560 | 0002_author_nicknames 561 | ======= 562 | 0002_longer_titles 563 | >>>>>>> 123456789 (Increase Book title length) 564 | """ 565 | ) 566 | ) 567 | 568 | out, err, returncode = self.call_command("testapp") 569 | 570 | assert out == ( 571 | "Renamed 0002_longer_titles.py to 0003_longer_titles.py," 572 | + " updated its dependencies, and updated max_migration.txt.\n" 573 | ) 574 | assert err == "" 575 | assert returncode == 0 576 | max_migration_txt = self.migrations_dir / "max_migration.txt" 577 | assert max_migration_txt.read_text() == "0003_longer_titles\n" 578 | 579 | assert not (self.migrations_dir / "0002_longer_titles.py").exists() 580 | new_content = (self.migrations_dir / "0003_longer_titles.py").read_text() 581 | assert new_content == dedent( 582 | """\ 583 | from django.db import migrations 584 | 585 | 586 | class Migration(migrations.Migration): 587 | dependencies = [ 588 | ("testapp", "0002_author_nicknames"), 589 | migrations.swappable_dependency("otherapp.0001_initial"), 590 | ] 591 | operations = [] 592 | """ 593 | ) 594 | 595 | 596 | class FindMigrationNamesTests(SimpleTestCase): 597 | def test_none_when_no_lines(self): 598 | result = module.find_migration_names([]) 599 | assert result is None 600 | 601 | def test_none_when_no_first_marker(self): 602 | result = module.find_migration_names(["not_a_marker", "0002_author_nicknames"]) 603 | assert result is None 604 | 605 | def test_none_when_no_second_marker(self): 606 | result = module.find_migration_names(["<<<<<<<", "0002_author_nicknames"]) 607 | assert result is None 608 | 609 | def test_works_with_two_way_merge_during_rebase(self): 610 | result = module.find_migration_names( 611 | [ 612 | "<<<<<<<", 613 | "0002_author_nicknames", 614 | "=======", 615 | "0002_longer_titles", 616 | ">>>>>>>", 617 | ] 618 | ) 619 | assert result == ("0002_author_nicknames", "0002_longer_titles") 620 | 621 | def test_works_with_three_way_merge_during_rebase(self): 622 | result = module.find_migration_names( 623 | [ 624 | "<<<<<<<", 625 | "0002_author_nicknames", 626 | "|||||||", 627 | "0001_initial", 628 | "=======", 629 | "0002_longer_titles", 630 | ">>>>>>>", 631 | ] 632 | ) 633 | assert result == ("0002_author_nicknames", "0002_longer_titles") 634 | 635 | def test_works_with_two_way_merge_during_merge(self): 636 | with mock.patch.object(module, "is_merge_in_progress", return_value=True): 637 | result = module.find_migration_names( 638 | [ 639 | "<<<<<<<", 640 | "0002_longer_titles", 641 | "=======", 642 | "0002_author_nicknames", 643 | ">>>>>>>", 644 | ] 645 | ) 646 | assert result == ("0002_author_nicknames", "0002_longer_titles") 647 | 648 | def test_works_with_three_way_merge_during_merge(self): 649 | with mock.patch.object(module, "is_merge_in_progress", return_value=True): 650 | result = module.find_migration_names( 651 | [ 652 | "<<<<<<<", 653 | "0002_longer_titles", 654 | "|||||||", 655 | "0001_initial", 656 | "=======", 657 | "0002_author_nicknames", 658 | ">>>>>>>", 659 | ] 660 | ) 661 | assert result == ("0002_author_nicknames", "0002_longer_titles") 662 | 663 | 664 | class IsMergeInProgressTests(SimpleTestCase): 665 | @pytest.fixture(autouse=True) 666 | def tmp_path_fixture(self, tmp_path): 667 | self.tmp_path = tmp_path 668 | self.subprocess_run = partial(subprocess.run, cwd=tmp_path, check=True) 669 | 670 | def setUp(self): 671 | orig = os.getcwd() 672 | os.chdir(self.tmp_path) 673 | self.addCleanup(os.chdir, orig) 674 | 675 | def test_no_git_command(self): 676 | with mock.patch.dict(os.environ, {"PATH": ""}): 677 | result = module.is_merge_in_progress() 678 | assert result is False 679 | 680 | def test_no_git_dir(self): 681 | result = module.is_merge_in_progress() 682 | assert result is False 683 | 684 | def test_git_dir_no_merge(self): 685 | self.subprocess_run(["git", "init"]) 686 | result = module.is_merge_in_progress() 687 | assert result is False 688 | 689 | def test_git_dir_merge(self): 690 | self.subprocess_run(["git", "init", "-b", "main"]) 691 | self.subprocess_run(["git", "config", "user.email", "hacker@example.com"]) 692 | self.subprocess_run(["git", "config", "user.name", "A Hacker"]) 693 | self.subprocess_run(["git", "commit", "--allow-empty", "-m", "A"]) 694 | self.subprocess_run(["git", "switch", "--orphan", "other"]) 695 | self.subprocess_run(["git", "commit", "--allow-empty", "-m", "B"]) 696 | self.subprocess_run( 697 | ["git", "merge", "--no-commit", "--allow-unrelated-histories", "main"] 698 | ) 699 | result = module.is_merge_in_progress() 700 | assert result is True 701 | 702 | 703 | class MigrationAppliedTests(TestCase): 704 | def test_table_does_not_exist(self): 705 | with connection.cursor() as cursor: 706 | cursor.execute("DROP TABLE django_migrations") 707 | 708 | result = module.migration_applied("testapp", "0001_initial") 709 | 710 | assert result is False 711 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.10" 4 | resolution-markers = [ 5 | "python_full_version >= '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60'", 6 | "python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60'", 7 | "extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 8 | "extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 9 | "extra != 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 10 | "extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 11 | "python_full_version >= '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 12 | "python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 13 | ] 14 | conflicts = [[ 15 | { package = "django-linear-migrations", group = "django42" }, 16 | { package = "django-linear-migrations", group = "django50" }, 17 | { package = "django-linear-migrations", group = "django51" }, 18 | { package = "django-linear-migrations", group = "django52" }, 19 | { package = "django-linear-migrations", group = "django60" }, 20 | ]] 21 | 22 | [[package]] 23 | name = "asgiref" 24 | version = "3.11.0" 25 | source = { registry = "https://pypi.org/simple" } 26 | dependencies = [ 27 | { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 28 | ] 29 | sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } 30 | wheels = [ 31 | { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, 32 | ] 33 | 34 | [[package]] 35 | name = "black" 36 | version = "25.12.0" 37 | source = { registry = "https://pypi.org/simple" } 38 | dependencies = [ 39 | { name = "click" }, 40 | { name = "mypy-extensions" }, 41 | { name = "packaging" }, 42 | { name = "pathspec" }, 43 | { name = "platformdirs" }, 44 | { name = "pytokens" }, 45 | { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 46 | { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 47 | ] 48 | sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/37/d5/8d3145999d380e5d09bb00b0f7024bf0a8ccb5c07b5648e9295f02ec1d98/black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8", size = 1895720, upload-time = "2025-12-08T01:46:58.197Z" }, 51 | { url = "https://files.pythonhosted.org/packages/06/97/7acc85c4add41098f4f076b21e3e4e383ad6ed0a3da26b2c89627241fc11/black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a", size = 1727193, upload-time = "2025-12-08T01:52:26.674Z" }, 52 | { url = "https://files.pythonhosted.org/packages/24/f0/fdf0eb8ba907ddeb62255227d29d349e8256ef03558fbcadfbc26ecfe3b2/black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea", size = 1774506, upload-time = "2025-12-08T01:46:25.721Z" }, 53 | { url = "https://files.pythonhosted.org/packages/e4/f5/9203a78efe00d13336786b133c6180a9303d46908a9aa72d1104ca214222/black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f", size = 1416085, upload-time = "2025-12-08T01:46:06.073Z" }, 54 | { url = "https://files.pythonhosted.org/packages/ba/cc/7a6090e6b081c3316282c05c546e76affdce7bf7a3b7d2c3a2a69438bd01/black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da", size = 1226038, upload-time = "2025-12-08T01:45:29.388Z" }, 55 | { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, 56 | { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, 57 | { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, 58 | { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, 59 | { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, 60 | { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, 61 | { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, 62 | { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, 63 | { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, 64 | { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, 65 | { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, 66 | { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, 67 | { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, 68 | { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, 69 | { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, 70 | { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, 71 | { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, 72 | { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, 73 | { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, 74 | { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, 75 | { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, 76 | ] 77 | 78 | [[package]] 79 | name = "click" 80 | version = "8.3.1" 81 | source = { registry = "https://pypi.org/simple" } 82 | dependencies = [ 83 | { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 84 | ] 85 | sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, 88 | ] 89 | 90 | [[package]] 91 | name = "colorama" 92 | version = "0.4.6" 93 | source = { registry = "https://pypi.org/simple" } 94 | 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" } 95 | wheels = [ 96 | { 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" }, 97 | ] 98 | 99 | [[package]] 100 | name = "coverage" 101 | version = "7.12.0" 102 | source = { registry = "https://pypi.org/simple" } 103 | sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } 104 | wheels = [ 105 | { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" }, 106 | { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" }, 107 | { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" }, 108 | { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" }, 109 | { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" }, 110 | { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" }, 111 | { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" }, 112 | { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" }, 113 | { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" }, 114 | { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" }, 115 | { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" }, 116 | { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" }, 117 | { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, 118 | { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, 119 | { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, 120 | { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, 121 | { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, 122 | { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, 123 | { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, 124 | { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, 125 | { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, 126 | { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, 127 | { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, 128 | { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, 129 | { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, 130 | { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, 131 | { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, 132 | { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, 133 | { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, 134 | { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, 135 | { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, 136 | { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, 137 | { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, 138 | { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, 139 | { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, 140 | { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, 141 | { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, 142 | { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, 143 | { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, 144 | { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, 145 | { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, 146 | { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, 147 | { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, 148 | { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, 149 | { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, 150 | { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, 151 | { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, 152 | { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, 153 | { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, 154 | { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, 155 | { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, 156 | { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, 157 | { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, 158 | { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, 159 | { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, 160 | { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, 161 | { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, 162 | { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, 163 | { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, 164 | { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, 165 | { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, 166 | { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, 167 | { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, 168 | { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, 169 | { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, 170 | { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, 171 | { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, 172 | { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, 173 | { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, 174 | { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, 175 | { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, 176 | { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, 177 | { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, 178 | { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, 179 | { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, 180 | { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, 181 | { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, 182 | { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, 183 | { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, 184 | { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, 185 | { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, 186 | { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, 187 | { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, 188 | { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, 189 | { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, 190 | { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, 191 | { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, 192 | { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, 193 | { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, 194 | { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, 195 | { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, 196 | ] 197 | 198 | [package.optional-dependencies] 199 | toml = [ 200 | { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 201 | ] 202 | 203 | [[package]] 204 | name = "django" 205 | version = "4.2.27" 206 | source = { registry = "https://pypi.org/simple" } 207 | dependencies = [ 208 | { name = "asgiref", marker = "extra == 'group-24-django-linear-migrations-django42' or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 209 | { name = "sqlparse", marker = "extra == 'group-24-django-linear-migrations-django42' or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 210 | { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-24-django-linear-migrations-django42') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 211 | ] 212 | sdist = { url = "https://files.pythonhosted.org/packages/ce/ff/6aa5a94b85837af893ca82227301ac6ddf4798afda86151fb2066d26ca0a/django-4.2.27.tar.gz", hash = "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92", size = 10432781, upload-time = "2025-12-02T14:01:49.006Z" } 213 | wheels = [ 214 | { url = "https://files.pythonhosted.org/packages/dd/f5/1a2319cc090870bfe8c62ef5ad881a6b73b5f4ce7330c5cf2cb4f9536b12/django-4.2.27-py3-none-any.whl", hash = "sha256:f393a394053713e7d213984555c5b7d3caeee78b2ccb729888a0774dff6c11a8", size = 7995090, upload-time = "2025-12-02T14:01:44.234Z" }, 215 | ] 216 | 217 | [[package]] 218 | name = "django" 219 | version = "5.0.14" 220 | source = { registry = "https://pypi.org/simple" } 221 | dependencies = [ 222 | { name = "asgiref", marker = "extra == 'group-24-django-linear-migrations-django50' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 223 | { name = "sqlparse", marker = "extra == 'group-24-django-linear-migrations-django50' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 224 | { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 225 | ] 226 | 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" } 227 | wheels = [ 228 | { 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" }, 229 | ] 230 | 231 | [[package]] 232 | name = "django" 233 | version = "5.1.15" 234 | source = { registry = "https://pypi.org/simple" } 235 | dependencies = [ 236 | { name = "asgiref", marker = "extra == 'group-24-django-linear-migrations-django51' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 237 | { name = "sqlparse", marker = "extra == 'group-24-django-linear-migrations-django51' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 238 | { name = "tzdata", marker = "(sys_platform == 'win32' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 239 | ] 240 | sdist = { url = "https://files.pythonhosted.org/packages/10/45/1ac68964193cfcc0b0912a0f68025d5bdb54f71ba7b8716e85b959874bd0/django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947", size = 10719662, upload-time = "2025-12-02T14:01:31.931Z" } 241 | wheels = [ 242 | { url = "https://files.pythonhosted.org/packages/27/79/372e091f0eba4ddb8228245ccd1baaa140e9658711f5e3a0056e540b4c1e/django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", size = 8260901, upload-time = "2025-12-02T14:01:27.352Z" }, 243 | ] 244 | 245 | [[package]] 246 | name = "django" 247 | version = "5.2.9" 248 | source = { registry = "https://pypi.org/simple" } 249 | resolution-markers = [ 250 | "python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60'", 251 | "extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 252 | "python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52' and extra != 'group-24-django-linear-migrations-django60'", 253 | ] 254 | dependencies = [ 255 | { name = "asgiref", marker = "(python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51') or extra == 'group-24-django-linear-migrations-django52' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 256 | { name = "sqlparse", marker = "(python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51') or extra == 'group-24-django-linear-migrations-django52' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 257 | { name = "tzdata", marker = "(python_full_version < '3.12' and sys_platform == 'win32' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51') or (sys_platform == 'win32' and extra == 'group-24-django-linear-migrations-django52') or (sys_platform != 'win32' and extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 258 | ] 259 | sdist = { url = "https://files.pythonhosted.org/packages/eb/1c/188ce85ee380f714b704283013434976df8d3a2df8e735221a02605b6794/django-5.2.9.tar.gz", hash = "sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495", size = 10848762, upload-time = "2025-12-02T14:01:08.418Z" } 260 | wheels = [ 261 | { url = "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a", size = 8290652, upload-time = "2025-12-02T14:01:03.485Z" }, 262 | ] 263 | 264 | [[package]] 265 | name = "django" 266 | version = "6.0" 267 | source = { registry = "https://pypi.org/simple" } 268 | resolution-markers = [ 269 | "python_full_version >= '3.12'", 270 | ] 271 | dependencies = [ 272 | { name = "asgiref", marker = "(python_full_version >= '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 273 | { name = "sqlparse", marker = "(python_full_version >= '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 274 | { name = "tzdata", marker = "(python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 275 | ] 276 | sdist = { url = "https://files.pythonhosted.org/packages/15/75/19762bfc4ea556c303d9af8e36f0cd910ab17dff6c8774644314427a2120/django-6.0.tar.gz", hash = "sha256:7b0c1f50c0759bbe6331c6a39c89ae022a84672674aeda908784617ef47d8e26", size = 10932418, upload-time = "2025-12-03T16:26:21.878Z" } 277 | wheels = [ 278 | { url = "https://files.pythonhosted.org/packages/d7/ae/f19e24789a5ad852670d6885f5480f5e5895576945fcc01817dfd9bc002a/django-6.0-py3-none-any.whl", hash = "sha256:1cc2c7344303bbfb7ba5070487c17f7fc0b7174bbb0a38cebf03c675f5f19b6d", size = 8339181, upload-time = "2025-12-03T16:26:16.231Z" }, 279 | ] 280 | 281 | [[package]] 282 | name = "django-linear-migrations" 283 | version = "2.19.0" 284 | source = { editable = "." } 285 | dependencies = [ 286 | { name = "django", version = "4.2.27", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-24-django-linear-migrations-django42' or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 287 | { name = "django", version = "5.0.14", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-24-django-linear-migrations-django50' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 288 | { name = "django", version = "5.1.15", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-24-django-linear-migrations-django51' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 289 | { name = "django", version = "5.2.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51') or extra == 'group-24-django-linear-migrations-django52' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60')" }, 290 | { name = "django", version = "6.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra != 'group-24-django-linear-migrations-django42' and extra != 'group-24-django-linear-migrations-django50' and extra != 'group-24-django-linear-migrations-django51' and extra != 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 291 | ] 292 | 293 | [package.dev-dependencies] 294 | django42 = [ 295 | { name = "django", version = "4.2.27", source = { registry = "https://pypi.org/simple" } }, 296 | ] 297 | django50 = [ 298 | { name = "django", version = "5.0.14", source = { registry = "https://pypi.org/simple" } }, 299 | ] 300 | django51 = [ 301 | { name = "django", version = "5.1.15", source = { registry = "https://pypi.org/simple" } }, 302 | ] 303 | django52 = [ 304 | { name = "django", version = "5.2.9", source = { registry = "https://pypi.org/simple" } }, 305 | ] 306 | django60 = [ 307 | { name = "django", version = "6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, 308 | ] 309 | test = [ 310 | { name = "black" }, 311 | { name = "coverage", extra = ["toml"] }, 312 | { name = "pytest" }, 313 | { name = "pytest-django" }, 314 | { name = "pytest-randomly" }, 315 | ] 316 | 317 | [package.metadata] 318 | requires-dist = [{ name = "django", specifier = ">=4.2" }] 319 | 320 | [package.metadata.requires-dev] 321 | django42 = [{ name = "django", marker = "python_full_version >= '3.8'", specifier = ">=4.2a1,<5" }] 322 | django50 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5a1,<5.1" }] 323 | django51 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5.1a1,<5.2" }] 324 | django52 = [{ name = "django", marker = "python_full_version >= '3.10'", specifier = ">=5.2a1,<6" }] 325 | django60 = [{ name = "django", marker = "python_full_version >= '3.12'", specifier = ">=6a1,<6.1" }] 326 | test = [ 327 | { name = "black" }, 328 | { name = "coverage", extras = ["toml"] }, 329 | { name = "pytest" }, 330 | { name = "pytest-django" }, 331 | { name = "pytest-randomly" }, 332 | ] 333 | 334 | [[package]] 335 | name = "exceptiongroup" 336 | version = "1.3.1" 337 | source = { registry = "https://pypi.org/simple" } 338 | dependencies = [ 339 | { name = "typing-extensions", marker = "python_full_version < '3.12' or (python_full_version == '3.12.*' and extra == 'group-24-django-linear-migrations-django42') or (python_full_version == '3.12.*' and extra == 'group-24-django-linear-migrations-django50') or (python_full_version == '3.12.*' and extra == 'group-24-django-linear-migrations-django51') or (python_full_version == '3.12.*' and extra == 'group-24-django-linear-migrations-django52') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (python_full_version >= '3.13' and extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 340 | ] 341 | sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } 342 | wheels = [ 343 | { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, 344 | ] 345 | 346 | [[package]] 347 | name = "iniconfig" 348 | version = "2.3.0" 349 | source = { registry = "https://pypi.org/simple" } 350 | sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } 351 | wheels = [ 352 | { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, 353 | ] 354 | 355 | [[package]] 356 | name = "mypy-extensions" 357 | version = "1.1.0" 358 | source = { registry = "https://pypi.org/simple" } 359 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 362 | ] 363 | 364 | [[package]] 365 | name = "packaging" 366 | version = "25.0" 367 | source = { registry = "https://pypi.org/simple" } 368 | 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" } 369 | wheels = [ 370 | { 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" }, 371 | ] 372 | 373 | [[package]] 374 | name = "pathspec" 375 | version = "0.12.1" 376 | source = { registry = "https://pypi.org/simple" } 377 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 378 | wheels = [ 379 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 380 | ] 381 | 382 | [[package]] 383 | name = "platformdirs" 384 | version = "4.5.1" 385 | source = { registry = "https://pypi.org/simple" } 386 | sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, 389 | ] 390 | 391 | [[package]] 392 | name = "pluggy" 393 | version = "1.6.0" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 398 | ] 399 | 400 | [[package]] 401 | name = "pygments" 402 | version = "2.19.2" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 407 | ] 408 | 409 | [[package]] 410 | name = "pytest" 411 | version = "9.0.2" 412 | source = { registry = "https://pypi.org/simple" } 413 | dependencies = [ 414 | { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 415 | { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 416 | { name = "iniconfig" }, 417 | { name = "packaging" }, 418 | { name = "pluggy" }, 419 | { name = "pygments" }, 420 | { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django50') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django42' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django51') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django50' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django52') or (extra == 'group-24-django-linear-migrations-django51' and extra == 'group-24-django-linear-migrations-django60') or (extra == 'group-24-django-linear-migrations-django52' and extra == 'group-24-django-linear-migrations-django60')" }, 421 | ] 422 | sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, 425 | ] 426 | 427 | [[package]] 428 | name = "pytest-django" 429 | version = "4.11.1" 430 | source = { registry = "https://pypi.org/simple" } 431 | dependencies = [ 432 | { name = "pytest" }, 433 | ] 434 | 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" } 435 | wheels = [ 436 | { 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" }, 437 | ] 438 | 439 | [[package]] 440 | name = "pytest-randomly" 441 | version = "4.0.1" 442 | source = { registry = "https://pypi.org/simple" } 443 | dependencies = [ 444 | { name = "pytest" }, 445 | ] 446 | sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } 447 | wheels = [ 448 | { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, 449 | ] 450 | 451 | [[package]] 452 | name = "pytokens" 453 | version = "0.3.0" 454 | source = { registry = "https://pypi.org/simple" } 455 | sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } 456 | wheels = [ 457 | { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, 458 | ] 459 | 460 | [[package]] 461 | name = "sqlparse" 462 | version = "0.5.4" 463 | source = { registry = "https://pypi.org/simple" } 464 | sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112, upload-time = "2025-11-28T07:10:18.377Z" } 465 | wheels = [ 466 | { url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" }, 467 | ] 468 | 469 | [[package]] 470 | name = "tomli" 471 | version = "2.3.0" 472 | source = { registry = "https://pypi.org/simple" } 473 | sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } 474 | wheels = [ 475 | { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, 476 | { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, 477 | { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, 478 | { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, 479 | { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, 480 | { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, 481 | { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, 482 | { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, 483 | { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, 484 | { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, 485 | { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, 486 | { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, 487 | { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, 488 | { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, 489 | { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, 490 | { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, 491 | { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, 492 | { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, 493 | { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, 494 | { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, 495 | { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, 496 | { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, 497 | { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, 498 | { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, 499 | { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, 500 | { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, 501 | { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, 502 | { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, 503 | { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, 504 | { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, 505 | { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, 506 | { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, 507 | { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, 508 | { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, 509 | { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, 510 | { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, 511 | { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, 512 | { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, 513 | { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, 514 | { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, 515 | { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, 516 | ] 517 | 518 | [[package]] 519 | name = "typing-extensions" 520 | version = "4.15.0" 521 | source = { registry = "https://pypi.org/simple" } 522 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 523 | wheels = [ 524 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 525 | ] 526 | 527 | [[package]] 528 | name = "tzdata" 529 | version = "2025.2" 530 | source = { registry = "https://pypi.org/simple" } 531 | 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" } 532 | wheels = [ 533 | { 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" }, 534 | ] 535 | --------------------------------------------------------------------------------