{{ transition.help_text }}
{% endblock %} django-commons-django-fsm-2-10256af/django_fsm/templates/django_fsm/fsm_transition_button.html 0000664 0000000 0000000 00000000257 15153477071 0032707 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/fsm_admin/ 0000775 0000000 0000000 00000000000 15153477071 0021104 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/fsm_admin/__init__.py 0000664 0000000 0000000 00000000000 15153477071 0023203 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/fsm_admin/mixins.py 0000664 0000000 0000000 00000001346 15153477071 0022771 0 ustar 00root root 0000000 0000000 from __future__ import annotations import typing from django.core import checks from django_fsm.admin import FSMAdminMixin as FSMTransitionMixin __all__ = ["FSMTransitionMixin"] @checks.register(checks.Tags.compatibility) def check_deprecated_mixin_import( app_configs: typing.Any, **kwargs: typing.Any ) -> list[checks.CheckMessage]: """ Check to warn users that they are still using the legacy import path. """ return [ checks.Warning( "'fsm_admin.mixins' is deprecated, Update your imports:", hint="Replace 'from fsm_admin.mixins import FSMTransitionMixin' " "with 'from django_fsm.admin import FSMAdminMixin'.", id="django_fsm.admin.W001", ) ] django-commons-django-fsm-2-10256af/mypy_stubs/ 0000775 0000000 0000000 00000000000 15153477071 0021365 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/mypy_stubs/django_fsm_log/ 0000775 0000000 0000000 00000000000 15153477071 0024335 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/mypy_stubs/django_fsm_log/__init__.pyi 0000664 0000000 0000000 00000000000 15153477071 0026605 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/mypy_stubs/django_fsm_log/decorators.pyi 0000664 0000000 0000000 00000000420 15153477071 0027221 0 ustar 00root root 0000000 0000000 from collections.abc import Callable from typing import ParamSpec from typing import TypeVar _P = ParamSpec("_P") _R = TypeVar("_R") def fsm_log_by(func: Callable[_P, _R]) -> Callable[_P, _R]: ... def fsm_log_description(func: Callable[_P, _R]) -> Callable[_P, _R]: ... django-commons-django-fsm-2-10256af/pyproject.toml 0000664 0000000 0000000 00000010627 15153477071 0022071 0 ustar 00root root 0000000 0000000 [project] name = "django-fsm-2" version = "4.2.1" description = "Django friendly finite state machine support." authors = [{ name = "Mikhail Podgurskiy", email = "kmmbvnr@gmail.com" }] requires-python = ">=3.10" readme = "README.md" license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Framework :: Django :: 6.0", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Typing :: Typed", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "django>=4.2.29", "django-stubs-ext", "typing-extensions; python_version < '3.12'", ] [project.urls] Homepage = "http://github.com/django-commons/django-fsm-2" Repository = "http://github.com/django-commons/django-fsm-2" Documentation = "http://github.com/django-commons/django-fsm-2" [project.optional-dependencies] graphviz = [ "graphviz", ] [dependency-groups] dev = [ "coverage", "django_fsm_log", "django-guardian", "graphviz", "mypy", "prek", "pytest", "pytest-cov", "pytest-django", "pytest-randomly", "tox", ] [tool.uv] default-groups = [ "dev" ] [tool.hatch.build.targets.sdist] include = ["django_fsm", "fsm_admin", "django_fsm/py.typed"] [tool.hatch.build.targets.wheel] include = ["django_fsm", "fsm_admin", "django_fsm/py.typed"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "tests.settings" [tool.ruff] line-length = 100 target-version = "py310" fix = true [tool.ruff.lint] select = ["ALL"] extend-ignore = [ "COM812", # This rule may cause conflicts when used with the formatter "D", # pydocstyle "DOC", # pydoclint "B", "PTH", "ANN", # Missing type annotation "S101", # Use of `assert` detected "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "ARG001", # Unused function argument "ARG002", # Unused method argument "TRY002", # Create your own exception "TRY003", # Avoid specifying long messages outside the exception class "EM101", # Exception must not use a string literal, assign to variable first "EM102", # Exception must not use an f-string literal, assign to variable first "SLF001", # Private member accessed "SIM103", # Return the condition directly "PLC0415", # `import` should be at the top-level of a file "PLR0913", # Too many arguments in function definition ] fixable = [ "I", # isort "RUF100", # Unused `noqa` directive "E501", ] [tool.ruff.lint.extend-per-file-ignores] "tests/*" = [ "DJ008", # Model does not define `__str__` method ] [tool.ruff.lint.isort] force-single-line = true required-imports = ["from __future__ import annotations"] [tool.django-stubs] django_settings_module = "tests.settings" [tool.mypy] python_version = 3.12 plugins = ["mypy_django_plugin.main"] mypy_path = "mypy_stubs" # Start off with these warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true # Getting these passing should be easy strict_equality = true extra_checks = true # Strongly recommend enabling this one as soon as you can check_untyped_defs = true # These shouldn't be too much additional work, but may be tricky to # get passing if you use a lot of untyped libraries disallow_subclassing_any = true disallow_untyped_decorators = true disallow_any_generics = true # These next few are various gradations of forcing use of type annotations disallow_untyped_calls = true disallow_incomplete_defs = true disallow_untyped_defs = true # This one isn't too hard to get passing, but return on investment is lower no_implicit_reexport = true # This one can be tricky to get passing if you use a lot of untyped libraries warn_return_any = true [[tool.mypy.overrides]] module = [ "tests.*", "django_fsm.tests.*" ] disallow_untyped_defs = false django-commons-django-fsm-2-10256af/tests/ 0000775 0000000 0000000 00000000000 15153477071 0020311 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/__init__.py 0000664 0000000 0000000 00000000000 15153477071 0022410 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/manage.py 0000775 0000000 0000000 00000001302 15153477071 0022112 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" from __future__ import annotations import os import sys def main() -> None: """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() django-commons-django-fsm-2-10256af/tests/settings.py 0000664 0000000 0000000 00000012574 15153477071 0022534 0 ustar 00root root 0000000 0000000 """ Django settings for tests project. Generated by 'django-admin startproject' using Django 4.2. For more information on this file, see https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ from __future__ import annotations import sys from pathlib import Path import django_stubs_ext django_stubs_ext.monkeypatch() # Enforce local path resolution to avoid using version declared as a dependency by django-fsm-log project_root = Path(__file__).resolve().parent.parent sys.path.insert(0, str(project_root)) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "nokey" # noqa: S105 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS: list[str] = [] # Application definition PROJECT_APPS = ( "django_fsm", "tests.testapp", ) INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django_fsm_log", "guardian", *PROJECT_APPS, ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "tests.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "tests.wsgi.application" # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # Authentication # https://docs.djangoproject.com/en/4.2/topics/auth/ AUTHENTICATION_BACKENDS = ( "django.contrib.auth.backends.ModelBackend", # this is default "guardian.backends.ObjectPermissionBackend", ) # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Django FSM-log settings DJANGO_FSM_LOG_IGNORED_MODELS = ( # "tests.testapp.models.AdminBlogPost", "tests.testapp.models.Application", "tests.testapp.models.BlogPost", "tests.testapp.models.DbState", "tests.testapp.models.FKApplication", "tests.testapp.tests.SimpleBlogPost", "tests.testapp.tests.test_abstract_inheritance.BaseAbstractModel", "tests.testapp.tests.test_abstract_inheritance.InheritedFromAbstractModel", "tests.testapp.tests.test_access_deferred_fsm_field.DeferrableModel", "tests.testapp.tests.test_basic_transitions.SimpleBlogPost", "tests.testapp.tests.test_conditions.BlogPostWithConditions", "tests.testapp.tests.test_custom_data.BlogPostWithCustomData", "tests.testapp.tests.test_exception_transitions.ExceptionalBlogPost", "tests.testapp.tests.test_graph_transitions.VisualBlogPost", "tests.testapp.tests.test_integer_field.BlogPostWithIntegerField", "tests.testapp.tests.test_lock_mixin.ExtendedBlogPost", "tests.testapp.tests.test_lock_mixin.LockedBlogPost", "tests.testapp.tests.test_mixin_support.MixinSupportTestModel", "tests.testapp.tests.test_multi_resultstate.MultiResultTest", "tests.testapp.tests.test_multidecorators.MultiDecoratedModel", "tests.testapp.tests.test_protected_field.ProtectedAccessModel", "tests.testapp.tests.test_protected_fields.RefreshableProtectedAccessModel", "tests.testapp.tests.test_proxy_inheritance.InheritedModel", "tests.testapp.tests.test_state_transitions.Caterpillar", "tests.testapp.tests.test_string_field_parameter.BlogPostWithStringField", "tests.testapp.tests.test_transition_all_except_target.ExceptTargetTransition", "tests.testapp.tests.test_key_field.FKBlogPost", ) django-commons-django-fsm-2-10256af/tests/testapp/ 0000775 0000000 0000000 00000000000 15153477071 0021771 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/testapp/__init__.py 0000664 0000000 0000000 00000000000 15153477071 0024070 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/testapp/admin.py 0000664 0000000 0000000 00000003565 15153477071 0023444 0 ustar 00root root 0000000 0000000 from __future__ import annotations import typing from django.contrib import admin from django_fsm_log.admin import StateLogInline import django_fsm as fsm from django_fsm.admin import FSMAdminMixin from .admin_forms import ForceStateForm from .admin_forms import FSMLogDescriptionForm from .models import AdminBlogPost if typing.TYPE_CHECKING: from django.db.models import QuerySet from django.http import HttpRequest @admin.register(AdminBlogPost) class AdminBlogPostAdmin(FSMAdminMixin, admin.ModelAdmin[AdminBlogPost]): list_display = ( "id", "title", "state", "step", ) actions = ["step_reset_action"] fsm_fields = [ "state", "step", ] fsm_forms = { "complex_transition": "tests.testapp.admin_forms.AdminBlogPostRenameModelForm", "invalid": FSMLogDescriptionForm, "force_state": ForceStateForm, } inlines = [StateLogInline] # Override label def get_fsm_label(self, transition): if transition.name == "do_something": return "My awesome transition" return super().get_fsm_label(transition) # Override help_text def get_help_text(self, transition): if transition.name == "do_something": return "Rename blog post" return super().get_help_text(transition) # Use a Transition as a Django admin action @admin.action(description="Reset step") def step_reset_action(self, request: HttpRequest, queryset: QuerySet[AdminBlogPost]) -> None: for obj in queryset: if fsm.can_proceed(obj.step_reset): self._apply_fsm_transition( obj=obj, transition_name="step_reset", request=request, kwargs={ "description": "Reset from admin", }, ) django-commons-django-fsm-2-10256af/tests/testapp/admin_forms.py 0000664 0000000 0000000 00000003202 15153477071 0024636 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django import forms from .models import AdminBlogPost from .models import AdminBlogPostState class FSMLogDescriptionForm(forms.Form): # fsm log field description = forms.CharField( label="Comment", widget=forms.Textarea, required=True, help_text="Why are you updating the title", ) class ForceStateForm(FSMLogDescriptionForm): state = forms.ChoiceField( choices=AdminBlogPostState.choices, required=True, ) class AdminBlogPostRenameForm(forms.Form): """ This form is used to test the admin form renaming functionality. """ title = forms.CharField( label="New Title", max_length=255, required=True, ) comment = forms.CharField( label="Comment", widget=forms.Textarea, required=True, help_text="Why are you updating the title", ) # fsm log field description = forms.CharField( label="Comment", widget=forms.Textarea, required=True, help_text="Why are you updating the title", ) class AdminBlogPostRenameModelForm(forms.ModelForm[AdminBlogPost]): """ This form is used to test the admin form renaming functionality. """ title = forms.CharField( label="New Title", max_length=255, required=True, ) # fsm log field description = forms.CharField( label="Comment", widget=forms.Textarea, required=True, help_text="Why are you updating the title", ) class Meta: model = AdminBlogPost fields: list[str] = ["title"] django-commons-django-fsm-2-10256af/tests/testapp/apps.py 0000664 0000000 0000000 00000000203 15153477071 0023301 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.apps import AppConfig class TestAppConfig(AppConfig): name = "tests.testapp" django-commons-django-fsm-2-10256af/tests/testapp/fixtures/ 0000775 0000000 0000000 00000000000 15153477071 0023642 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/testapp/fixtures/test_states_data.json 0000664 0000000 0000000 00000001056 15153477071 0030072 0 ustar 00root root 0000000 0000000 [ { "model": "testapp.dbstate", "pk": "new", "fields": { "label": "_New"} }, { "model": "testapp.dbstate", "pk": "draft", "fields": { "label": "_Draft"} }, { "model": "testapp.dbstate", "pk": "dept", "fields": { "label": "_Dept"} }, { "model": "testapp.dbstate", "pk": "dean", "fields": { "label": "_Dean"} }, { "model": "testapp.dbstate", "pk": "done", "fields": { "label": "_Done"} } ] django-commons-django-fsm-2-10256af/tests/testapp/models.py 0000664 0000000 0000000 00000031135 15153477071 0023631 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.contrib.auth.models import AbstractUser from django.db import models from django_fsm_log.decorators import fsm_log_by from django_fsm_log.decorators import fsm_log_description import django_fsm as fsm from django_fsm import GET_STATE from django_fsm import RETURN_VALUE from django_fsm import FSMField from django_fsm import FSMKeyField from django_fsm import transition class Application(models.Model): """ Student application need to be approved by dept chair and dean. Test workflow """ state = FSMField(default="new") @transition(field=state, source="new", target="published", on_error="failed") def standard(self) -> None: pass @transition(field=state, source="published") def no_target(self) -> None: pass @transition(field=state, source="*", target="blocked") def any_source(self) -> None: pass @transition(field=state, source="+", target="hidden") def any_source_except_target(self) -> None: pass @transition( field=state, source="new", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state(self, *, allowed: bool) -> None: pass @transition( field=state, source="*", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state_any_source(self, *, allowed: bool) -> None: pass @transition( field=state, source="+", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state_any_source_except_target(self, *, allowed: bool) -> None: pass @transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked")) def return_value(self) -> str: return "published" @transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked")) def return_value_any_source(self) -> str: return "published" @transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked")) def return_value_any_source_except_target(self) -> str: return "published" @transition(field=state, source="new", target="published", on_error="failed") def on_error(self) -> None: pass class DbState(models.Model): """ States in DB """ id = models.CharField(primary_key=True, max_length=50) label = models.CharField(max_length=255) def __str__(self): return self.label class FKApplication(models.Model): """ Student application need to be approved by dept chair and dean. Test workflow for FSMKeyField """ state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE) @transition(field=state, source="new", target="published") def standard(self) -> None: pass @transition(field=state, source="published") def no_target(self) -> None: pass @transition(field=state, source="*", target="blocked") def any_source(self) -> None: pass @transition(field=state, source="+", target="hidden") def any_source_except_target(self) -> None: pass @transition( field=state, source="new", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state(self, *, allowed: bool) -> None: pass @transition( field=state, source="*", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state_any_source(self, *, allowed: bool) -> None: pass @transition( field=state, source="+", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def get_state_any_source_except_target(self, *, allowed: bool) -> None: pass @transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked")) def return_value(self) -> str: return "published" @transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked")) def return_value_any_source(self) -> str: return "published" @transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked")) def return_value_any_source_except_target(self) -> str: return "published" @transition(field=state, source="new", target="published", on_error="failed") def on_error(self) -> None: pass class MultiStateApplication(Application): another_state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE) @transition(field=another_state, source="new", target="published") def another_state_standard(self) -> None: pass class BlogPostState(models.IntegerChoices): NEW = 0, "New" PUBLISHED = 1, "Published" HIDDEN = 2, "Hidden" REMOVED = 3, "Removed" RESTORED = 4, "Restored" MODERATED = 5, "Moderated" STOLEN = 6, "Stolen" FAILED = 7, "Failed" class BlogPost(models.Model): """ Test workflow """ state = FSMField(choices=BlogPostState.choices, default=BlogPostState.NEW, protected=True) class Meta: permissions = [ ("can_publish_post", "Can publish post"), ("can_remove_post", "Can remove post"), ] def can_restore(self: models.Model, user: fsm.UserWithPermissions) -> bool: if isinstance(user, AbstractUser): return bool(user.is_superuser or user.is_staff) return False @transition( field=state, source=BlogPostState.NEW, target=BlogPostState.PUBLISHED, on_error=BlogPostState.FAILED, permission="testapp.can_publish_post", ) def publish(self) -> None: pass @transition(field=state, source=BlogPostState.PUBLISHED) def notify_all(self) -> None: pass @transition( field=state, source=BlogPostState.PUBLISHED, target=BlogPostState.HIDDEN, on_error=BlogPostState.FAILED, ) def hide(self) -> None: pass @transition( field=state, source=BlogPostState.NEW, target=BlogPostState.REMOVED, on_error=BlogPostState.FAILED, permission=lambda _, u: u.has_perm("testapp.can_remove_post"), ) def remove(self) -> None: raise Exception(f"No rights to delete {self}") @transition( field=state, source=BlogPostState.NEW, target=BlogPostState.RESTORED, on_error=BlogPostState.FAILED, permission=can_restore, ) def restore(self) -> None: pass @transition( field=state, source=[BlogPostState.PUBLISHED, BlogPostState.HIDDEN], target=BlogPostState.STOLEN, ) def steal(self) -> None: pass @transition(field=state, source="*", target=BlogPostState.MODERATED) def moderate(self) -> None: pass class AdminBlogPostState(models.TextChoices): CREATED = "created", "Created" REVIEWED = "reviewed", "Reviewed" PUBLISHED = "published", "Published" HIDDEN = "hidden", "Hidden" class AdminBlogPostStep(models.TextChoices): STEP_1 = "step1", "Step one" STEP_2 = "step2", "Step two" STEP_3 = "step3", "Step three" class AdminBlogPost(fsm.FSMModelMixin, models.Model): title = models.CharField(max_length=50) state = FSMField( choices=AdminBlogPostState.choices, default=AdminBlogPostState.CREATED, protected=True, ) step = FSMField( choices=AdminBlogPostStep.choices, default=AdminBlogPostStep.STEP_1, protected=False, ) # state transitions def __str__(self) -> str: return f"{self.title} ({self.state})" @fsm_log_by @fsm_log_description @transition( field=state, source="*", target=RETURN_VALUE(*AdminBlogPostState), ) def force_state( self, state: AdminBlogPostState, by: AbstractUser | None = None, description: str | None = None, ) -> AdminBlogPostState: return state @fsm_log_by @fsm_log_description @transition( field=state, source="*", target=AdminBlogPostState.HIDDEN, custom={ "admin": False, }, ) def secret_transition( self, by: AbstractUser | None = None, description: str | None = None ) -> None: pass @fsm_log_by @fsm_log_description @transition( field=state, source=AdminBlogPostState.CREATED, target=AdminBlogPostState.REVIEWED, ) def moderate(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass @fsm_log_by @fsm_log_description @transition( field=state, source=[ AdminBlogPostState.REVIEWED, AdminBlogPostState.HIDDEN, ], target=AdminBlogPostState.PUBLISHED, ) def publish(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass @fsm_log_by @fsm_log_description @transition( field=state, source=[ AdminBlogPostState.REVIEWED, AdminBlogPostState.PUBLISHED, ], target=AdminBlogPostState.HIDDEN, ) def hide(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass @fsm_log_by @fsm_log_description @transition( field=state, source="*", target=None, ) def invalid(self, by: AbstractUser | None = None, description: str | None = None) -> None: raise Exception("You shall not pass!") @transition( field=state, source="*", target=None, ) def non_fsm_log_invalid(self) -> None: raise Exception("Domain-raised exception") @fsm_log_by @fsm_log_description @transition( field=state, source="*", target=None, ) def invalid_without_forms( self, by: AbstractUser | None = None, description: str | None = None ) -> None: raise Exception("You shall not pass!") @fsm_log_by @fsm_log_description @transition( field=state, source="*", target=AdminBlogPostState.CREATED, custom={ "label": "Rename *", "form": "tests.testapp.admin_forms.AdminBlogPostRenameForm", "help_text": "Do it wisely!", }, ) def complex_transition( self, *, title: str, comment: str | None = None, by: AbstractUser | None = None, description: str | None = None, ) -> None: self.title = title if comment: ... # Do something with the comment @fsm_log_by @fsm_log_description @transition(field=state, source="*", target=None, conditions=[lambda _obj: False]) def conditions_unmet( self, by: AbstractUser | None = None, description: str | None = None ) -> None: pass @fsm_log_by @fsm_log_description @transition(field=state, source="*", target=None, permission=lambda _obj, _user: False) def permission_denied( self, by: AbstractUser | None = None, description: str | None = None ) -> None: pass # step transitions @fsm_log_by @fsm_log_description @transition( field=step, source=[AdminBlogPostStep.STEP_1], target=AdminBlogPostStep.STEP_2, custom={ "label": "Go to Step 2", }, ) def step_two(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass @fsm_log_by @fsm_log_description @transition( field=step, source=[AdminBlogPostStep.STEP_2], target=AdminBlogPostStep.STEP_3, ) def step_three(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass @fsm_log_by @fsm_log_description @transition( field=step, source=[ AdminBlogPostStep.STEP_2, AdminBlogPostStep.STEP_3, ], target=AdminBlogPostStep.STEP_1, ) def step_reset(self, by: AbstractUser | None = None, description: str | None = None) -> None: pass def normal_function(self): raise NotImplementedError @property def name_property(self): return "name" django-commons-django-fsm-2-10256af/tests/testapp/tests/ 0000775 0000000 0000000 00000000000 15153477071 0023133 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/testapp/tests/__init__.py 0000664 0000000 0000000 00000000000 15153477071 0025232 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-10256af/tests/testapp/tests/test_abstract_inheritance.py 0000664 0000000 0000000 00000003562 15153477071 0030726 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import can_proceed from django_fsm import transition class BaseAbstractModel(models.Model): state = FSMField(default="new") class Meta: abstract = True @transition(field=state, source="new", target="published") def publish(self): pass class AnotherFromAbstractModel(BaseAbstractModel): """ This class exists to trigger a regression when multiple concrete classes inherit from a shared abstract class (example: BaseAbstractModel). Don't try to remove it. """ @transition(field="state", source="published", target="sticked") def stick(self): pass class InheritedFromAbstractModel(BaseAbstractModel): @transition(field="state", source="published", target="sticked") def stick(self): pass class TestinheritedModel(TestCase): def setUp(self): self.model = InheritedFromAbstractModel() def test_known_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() assert self.model.state == "published" assert can_proceed(self.model.stick) self.model.stick() assert self.model.state == "sticked" def test_field_available_transitions_works(self): self.model.publish() assert self.model.state == "published" transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] assert [data.target for data in transitions] == ["sticked"] def test_field_all_transitions_works(self): transitions = self.model.get_all_state_transitions() # type: ignore[attr-defined] assert {("new", "published"), ("published", "sticked")} == { (data.source, data.target) for data in transitions } django-commons-django-fsm-2-10256af/tests/testapp/tests/test_access_deferred_fsm_field.py 0000664 0000000 0000000 00000001633 15153477071 0031660 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import can_proceed from django_fsm import transition class DeferrableModel(models.Model): state = FSMField(default="new") objects: models.Manager[DeferrableModel] = models.Manager() @transition(field=state, source="new", target="published") def publish(self): pass @transition(field=state, source="+", target="removed") def remove(self): pass class Test(TestCase): def setUp(self): DeferrableModel.objects.create() self.model = DeferrableModel.objects.only("id").get() def test_usecase(self): assert self.model.state == "new" assert can_proceed(self.model.remove) self.model.remove() assert self.model.state == "removed" assert not can_proceed(self.model.remove) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_admin.py 0000664 0000000 0000000 00000072446 15153477071 0025651 0 ustar 00root root 0000000 0000000 from __future__ import annotations import contextlib import typing from http import HTTPStatus from unittest import mock from unittest.mock import patch import pytest from django.contrib import admin from django.contrib import messages from django.contrib.admin.sites import AdminSite from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from django.http import HttpResponseBadRequest from django.http import HttpResponseRedirect from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import modify_settings from django.urls import reverse from django_fsm_log.models import StateLog import django_fsm as fsm from django_fsm.admin import FSMAdminMixin from tests.testapp.admin import AdminBlogPostAdmin from tests.testapp.admin_forms import AdminBlogPostRenameModelForm from tests.testapp.admin_forms import FSMLogDescriptionForm from tests.testapp.models import AdminBlogPost from tests.testapp.models import AdminBlogPostState if typing.TYPE_CHECKING: from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import User from django.core.handlers.wsgi import WSGIRequest disable_fsm_log = modify_settings( DJANGO_FSM_LOG_IGNORED_MODELS={ "append": "tests.testapp.models.AdminBlogPost", } ) class BaseAdminTestCase(TestCase): model = AdminBlogPost admin_class = AdminBlogPostAdmin user: User | AnonymousUser blog_post: AdminBlogPost fsm_log_enabled = True @classmethod def setUpTestData(cls): cls.user = get_user_model().objects.create_user( username="jacob", password="password", # noqa: S106 is_staff=True, ) cls.blog_post = cls.model.objects.create( title="Article name", state=AdminBlogPostState.PUBLISHED ) def setUp(self) -> None: self.model_admin = self.admin_class(self.model, AdminSite()) def make_request(self, *, data: typing.Any | None = None) -> WSGIRequest: factory = RequestFactory() request = factory.post(path="/", data=data or {}) request.user = self.user return request def assert_state_log_empty(self) -> None: assert StateLog.objects.count() == 0 def assert_state_log_for_user( self, *, description: str | None = None, source_state: AdminBlogPostState | None = None, state: AdminBlogPostState | None = None, transition: str | None = None, ) -> None: if self.fsm_log_enabled: statelog = StateLog.objects.get() assert statelog.by == self.user if description is not None: assert statelog.description == description if source_state is not None: assert statelog.source_state == source_state if state is not None: assert statelog.state == state if transition is not None: assert statelog.transition == transition else: self.assert_state_log_empty() class EmptyFieldAdmin(FSMAdminMixin, admin.ModelAdmin[AdminBlogPost]): fsm_fields = [] class InvalidFieldAdmin(FSMAdminMixin, admin.ModelAdmin[AdminBlogPost]): fsm_fields = ["title"] class InvalidFormPathAdmin(FSMAdminMixin, admin.ModelAdmin[AdminBlogPost]): fsm_fields = ["state"] fsm_forms = { "complex_transition": "invalid.path", } class ModelAdminMisconfigurationTestCase(TestCase): user: User | AnonymousUser blog_post: AdminBlogPost request: WSGIRequest def setUp(self) -> None: with contextlib.suppress(admin.sites.NotRegistered): # type: ignore[attr-defined] admin.site.unregister(AdminBlogPost) def tearDown(self) -> None: """Reconnect original admin""" with contextlib.suppress(admin.sites.NotRegistered): # type: ignore[attr-defined] admin.site.unregister(AdminBlogPost) admin.site.register(AdminBlogPost, AdminBlogPostAdmin) @classmethod def setUpTestData(cls): cls.user = get_user_model().objects.create_user( username="jacob", password="password", # noqa: S106 is_staff=True, ) cls.blog_post = AdminBlogPost.objects.create( title="Article name", state=AdminBlogPostState.PUBLISHED ) cls.request = RequestFactory().post(path="/") cls.request.user = cls.user def test_empty_fsm_field(self): with pytest.raises(ImproperlyConfigured): admin.site.register(AdminBlogPost, EmptyFieldAdmin) def test_invalid_fsm_field(self): admin.site.register(AdminBlogPost, InvalidFieldAdmin) with pytest.raises(ImproperlyConfigured, match="'title' is not an FSMField"): InvalidFieldAdmin(AdminBlogPost, AdminSite()).get_readonly_fields( request=self.request, obj=self.blog_post, ) def test_invalid_form_path(self): admin.site.register(AdminBlogPost, InvalidFormPathAdmin) blog_admin = InvalidFormPathAdmin(AdminBlogPost, AdminSite()) transition = blog_admin._get_fsm_transition_by_name( obj=self.blog_post, transition_name="complex_transition" ) with pytest.raises( ImproperlyConfigured, match=r"Failed to import form invalid\.path", ): blog_admin.get_fsm_transition_form(transition) class ModelAdminTestCase(TestCase): blog_post: AdminBlogPost request: WSGIRequest @classmethod def setUpTestData(cls): blog_post = AdminBlogPost.objects.create(title="Article name") blog_post.moderate() blog_post.save() cls.blog_post = blog_post cls.request = RequestFactory().get(path="/path") cls.request.user = get_user_model().objects.create_user( username="jacob", password="password", # noqa: S106 is_staff=True, ) def setUp(self): self.model_admin = AdminBlogPostAdmin(AdminBlogPost, AdminSite()) # Basics def test_protected_fields_are_readonly(self): assert self.model_admin.get_readonly_fields(request=self.request) == ("state",) # Execution def test_execute_fsm_transition_falls_back_to_plain_call(self) -> None: called: dict[str, str] = {} def transition_method(*, comment: str) -> None: called["comment"] = comment with mock.patch.object(self.model_admin, "_is_fsm_log_enabled", return_value=False): self.model_admin._execute_fsm_transition( transition_func=transition_method, request=self.request, kwargs={"comment": "Because"}, ) assert called["comment"] == "Because" # Context def test_get_fsm_extra_context_filters_admin_hidden( self, ) -> None: blog_post = AdminBlogPost.objects.create( title="Article name", state=AdminBlogPostState.REVIEWED, ) transitions = list( self.model_admin._get_fsm_extra_context(request=self.request, obj=blog_post) ) assert len(transitions) == 2 # noqa: PLR2004 transitions_by_field = { item.fsm_field: { transition_context.name for transition_context in item.available_transitions } for item in transitions } # Only available transitions assert transitions_by_field["step"] == {"step_two"} # but not if custom.admin is False assert "secret_transition" in blog_post.get_available_user_state_transitions( # type: ignore[attr-defined] user=self.request.user ) assert "secret_transition" not in transitions_by_field["state"] assert "conditions_unmet" not in transitions_by_field["state"] assert "permission_denied" not in transitions_by_field["state"] @mock.patch("django.contrib.admin.ModelAdmin.change_view") @mock.patch("django_fsm.admin.FSMAdminMixin._get_fsm_extra_context") def test_change_view_context( self, mock_get_fsm_extra_context: mock.Mock, mock_super_change_view: mock.Mock, ) -> None: mock_get_fsm_extra_context.return_value = ["object transitions"] self.model_admin.change_view( request=self.request, form_url="/test", object_id=str(self.blog_post.pk), extra_context={ "existing_context": "existing context", }, ) mock_get_fsm_extra_context.assert_called_once_with( request=self.request, obj=self.blog_post, ) mock_super_change_view.assert_called_once_with( request=self.request, object_id=str(self.blog_post.pk), form_url="/test", extra_context={ "existing_context": "existing context", "fsm_object_transitions": ["object transitions"], }, ) @patch("django.contrib.admin.options.ModelAdmin.message_user") class ResponseChangeViewTestCase(BaseAdminTestCase): # Classic updates @mock.patch("django.contrib.admin.ModelAdmin.response_change") def test_classic_update_keep_working( self, mock_response_change: mock.Mock, mock_message_user: mock.Mock ) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED self.model_admin.response_change( request=self.make_request( data={"title": "New Name"}, ), obj=blog_post, ) mock_response_change.assert_called_once() self.assert_state_log_empty() # Errors def test_unknown_transition(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED with pytest.raises(AttributeError): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "unknown_transition"}, ), obj=blog_post, ) self.assert_state_log_empty() def test_non_transition_function(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with pytest.raises(ValueError, match=r"not an FSM transition"): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "normal_function"}, ), obj=self.blog_post, ) self.assert_state_log_empty() def test_non_callable_function(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with pytest.raises(TypeError, match=r"Attribute 'name_property' is not callable."): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "name_property"}, ), obj=self.blog_post, ) self.assert_state_log_empty() def test_transition_raised_exception(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "invalid_without_forms"}, ), obj=self.blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'invalid_without_forms' failed: You shall not pass!.", level=messages.ERROR, ) self.assert_state_log_empty() def test_execute_fsm_transition_falls_back_to_plain_call( self, mock_message_user: mock.Mock ) -> None: self.assert_state_log_empty() self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "non_fsm_log_invalid"}, ), obj=self.blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'non_fsm_log_invalid' failed: Domain-raised exception.", level=messages.ERROR, ) self.assert_state_log_empty() def test_response_change_transition_not_allowed(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED with mock.patch.object( self.model_admin, "_execute_fsm_transition", side_effect=fsm.TransitionNotAllowed, ): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "moderate"}, ), obj=blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'moderate' is not allowed.", level=messages.ERROR, ) blog_post.refresh_from_db() assert blog_post.state == AdminBlogPostState.CREATED self.assert_state_log_empty() def test_response_change_concurrent_transition(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED with mock.patch.object( self.model_admin, "_execute_fsm_transition", side_effect=fsm.ConcurrentTransition("error message"), ): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "moderate"}, ), obj=blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'moderate' failed: error message.", level=messages.ERROR, ) blog_post.refresh_from_db() assert blog_post.state == AdminBlogPostState.CREATED self.assert_state_log_empty() # Transitions def test_transition_applied(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "moderate"}, ), obj=blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'moderate' succeeded.", level=messages.SUCCESS, ) blog_post.refresh_from_db() assert blog_post.state == AdminBlogPostState.REVIEWED self.assert_state_log_for_user() def test_transition_not_allowed_exception(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "publish"}, ), obj=blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'publish' is not allowed.", level=messages.ERROR, ) blog_post.refresh_from_db() assert blog_post.state == AdminBlogPostState.CREATED self.assert_state_log_empty() def test_concurrent_transition_exception(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() blog_post = AdminBlogPost.objects.create(title="Article name") assert blog_post.state == AdminBlogPostState.CREATED with mock.patch( "tests.testapp.models.AdminBlogPost.moderate", side_effect=fsm.ConcurrentTransition("error message"), ): self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "moderate"}, ), obj=blog_post, ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'moderate' failed: error message.", level=messages.ERROR, ) blog_post.refresh_from_db() assert blog_post.state == AdminBlogPostState.CREATED self.assert_state_log_empty() def test_unknown_transition_raise_error(self, mock_message_user: mock.Mock) -> None: request = self.make_request( data={"_fsm_transition_to": "unknown_transition"}, ) with pytest.raises(AttributeError): self.model_admin.response_change( request=request, obj=self.blog_post, ) def test_transition_without_form_execute_fsm_transition( self, mock_message_user: mock.Mock ) -> None: self.assert_state_log_empty() res = self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "hide"}, ), obj=self.blog_post, ) assert isinstance(res, HttpResponseRedirect) assert res.status_code == HTTPStatus.FOUND mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'hide' succeeded.", level=messages.SUCCESS, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.HIDDEN self.assert_state_log_for_user() def test_transition_with_form_redirects_properly(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() res = self.model_admin.response_change( request=self.make_request( data={"_fsm_transition_to": "complex_transition"}, ), obj=self.blog_post, ) assert isinstance(res, HttpResponseRedirect) assert res.status_code == HTTPStatus.FOUND assert res.url == reverse( f"admin:{self.model_admin.opts.app_label}_{self.model_admin.opts.model_name}_transition", kwargs={ "object_id": self.blog_post.pk, "transition_name": "complex_transition", }, ) self.assert_state_log_empty() self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.PUBLISHED assert self.blog_post.title == "Article name" mock_message_user.assert_not_called() @disable_fsm_log class ResponseChangeViewWithoutFsmLogTestCase(ResponseChangeViewTestCase): fsm_log_enabled = False @mock.patch("tests.testapp.admin.AdminBlogPostAdmin.message_user") class TransitionViewTestCase(BaseAdminTestCase): # Errors def test_non_transition_function(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with pytest.raises(ValueError, match=r"not an FSM transition"): self.model_admin.fsm_transition_view( request=self.make_request(), object_id=str(self.blog_post.pk), transition_name="normal_function", ) self.assert_state_log_empty() def test_non_callable_function(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with pytest.raises(TypeError, match=r"Attribute 'name_property' is not callable."): self.model_admin.fsm_transition_view( request=self.make_request(), object_id=str(self.blog_post.pk), transition_name="name_property", ) self.assert_state_log_empty() def test_transition_raised_exception(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() self.model_admin.fsm_transition_view( request=self.make_request( data={ "description": "because", }, ), object_id=str(self.blog_post.pk), transition_name="invalid", ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'invalid' failed: You shall not pass!.", level=messages.ERROR, ) self.assert_state_log_empty() def test_transition_form_not_allowed(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with mock.patch.object( self.model_admin, "_execute_fsm_transition", side_effect=fsm.TransitionNotAllowed ): self.model_admin.fsm_transition_view( request=self.make_request( data={ "title": "New Title", "comment": "Because", "description": "Because", }, ), object_id=str(self.blog_post.pk), transition_name="complex_transition", ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'complex_transition' is not allowed.", level=messages.ERROR, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.PUBLISHED self.assert_state_log_empty() def test_transition_form_concurrent_exception(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with mock.patch.object( self.model_admin, "_execute_fsm_transition", side_effect=fsm.ConcurrentTransition("error message"), ): self.model_admin.fsm_transition_view( request=self.make_request( data={ "title": "New Title", "comment": "Because", "description": "Because", }, ), object_id=str(self.blog_post.pk), transition_name="complex_transition", ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'complex_transition' failed: error message.", level=messages.ERROR, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.PUBLISHED self.assert_state_log_empty() def test_permission_denied(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with mock.patch.object( self.model_admin, "fsm_forms", { "permission_denied": FSMLogDescriptionForm, }, ): self.model_admin.fsm_transition_view( request=self.make_request( data={ "description": "Because", }, ), object_id=str(self.blog_post.pk), transition_name="permission_denied", ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'permission_denied' is not allowed.", level=messages.ERROR, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.PUBLISHED self.assert_state_log_empty() def test_conditions_denied(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() with mock.patch.object( self.model_admin, "fsm_forms", { "conditions_unmet": FSMLogDescriptionForm, }, ): self.model_admin.fsm_transition_view( request=self.make_request( data={ "description": "Because", }, ), object_id=str(self.blog_post.pk), transition_name="conditions_unmet", ) mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'conditions_unmet' is not allowed.", level=messages.ERROR, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.PUBLISHED self.assert_state_log_empty() def test_invalid_object_id(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() invalid_object_id = "123456" assert not AdminBlogPost.objects.filter(pk=invalid_object_id).exists() res = self.model_admin.fsm_transition_view( request=self.make_request( data={ "state": AdminBlogPostState.CREATED, "description": "because", }, ), object_id=invalid_object_id, transition_name="force_state", ) assert isinstance(res, HttpResponseRedirect) assert res.status_code == HTTPStatus.FOUND assert res["Location"] == reverse("admin:index") mock_message_user.assert_called_once_with( mock.ANY, f"{self.model._meta.verbose_name} with ID “{invalid_object_id}” doesn’t exist. Perhaps it was deleted?", # noqa: RUF001, E501 messages.WARNING, ) self.assert_state_log_empty() def test_transition_without_form_(self, mock_message_user: mock.Mock) -> None: self.assert_state_log_empty() res = self.model_admin.fsm_transition_view( request=self.make_request(), transition_name="hide", object_id=str(self.blog_post.pk), ) assert isinstance(res, HttpResponseBadRequest) assert res.status_code == HTTPStatus.BAD_REQUEST self.assert_state_log_empty() # Form submissions def test_transition_form_submission_executes(self, mock_message_user: mock.Mock) -> None: assert self.blog_post.state == AdminBlogPostState.PUBLISHED self.assert_state_log_empty() res = self.model_admin.fsm_transition_view( request=self.make_request( data={ "title": "New Title", "comment": "Because", "description": "Because", }, ), object_id=str(self.blog_post.pk), transition_name="complex_transition", ) assert isinstance(res, HttpResponseRedirect) assert res.status_code == HTTPStatus.FOUND mock_message_user.assert_called_once_with( request=mock.ANY, message="FSM transition 'complex_transition' succeeded.", level=messages.SUCCESS, ) self.blog_post.refresh_from_db() assert self.blog_post.state == AdminBlogPostState.CREATED assert self.blog_post.title == "New Title" self.assert_state_log_for_user( description="Because", source_state=AdminBlogPostState.PUBLISHED, state=AdminBlogPostState.CREATED, transition="complex_transition", ) # Rendering def test_transition_form_rendered(self, mock_message_user: mock.Mock) -> None: request = RequestFactory().get(path="/") request.user = self.user mock_response = HttpResponse("ok") with mock.patch("django_fsm.admin.render", return_value=mock_response) as mock_render: res = self.model_admin.fsm_transition_view( request=request, object_id=str(self.blog_post.pk), transition_name="complex_transition", ) assert res is mock_response mock_render.assert_called_once() args, kwargs = mock_render.call_args assert args[0] is request assert kwargs["template_name"] == self.model_admin.fsm_transition_form_template context = kwargs["context"] assert "transition_form" in context assert context["transition_form"].is_bound is False class ModelFormTransitionViewTestCase(TransitionViewTestCase): """ Runs TransitionViewTestCase but with **class-based forms** (instead of import path) """ def setUp(self) -> None: self.fsm_forms_patcher = mock.patch.dict( AdminBlogPostAdmin.fsm_forms, { "complex_transition": AdminBlogPostRenameModelForm, "invalid": FSMLogDescriptionForm, }, clear=False, ) self.fsm_forms_patcher.start() super().setUp() def tearDown(self) -> None: self.fsm_forms_patcher.stop() super().tearDown() @disable_fsm_log class NoFsmLogTransitionViewTestCase(TransitionViewTestCase): """ Runs TransitionViewTestCase but with **FSM log disabled** """ fsm_log_enabled = False @disable_fsm_log class NoFsmLogModelFormTransitionViewTestCase(ModelFormTransitionViewTestCase): """ Runs TransitionViewTestCase but with **class-based forms** and **FSM log disabled** """ fsm_log_enabled = False django-commons-django-fsm-2-10256af/tests/testapp/tests/test_basic_transitions.py 0000664 0000000 0000000 00000026074 15153477071 0030273 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import Transition from django_fsm import TransitionNotAllowed from django_fsm import can_proceed from django_fsm import transition from django_fsm.signals import post_transition from django_fsm.signals import pre_transition class SimpleBlogPost(models.Model): state = FSMField(default="new") @transition(field=state, source="new", target="published") def publish(self): pass @transition(source="published", field=state) def notify_all(self): pass @transition(source="published", target="hidden", field=state) def hide(self): pass @transition(source="new", target="removed", field=state) def remove(self): raise Exception("Upss") @transition(source=["published", "hidden"], target="stolen", field=state) def steal(self): pass @transition(source="*", target="moderated", field=state) def moderate(self): pass @transition(source="+", target="blocked", field=state) def block(self): pass @transition(source="*", target="", field=state) def empty(self): pass class AdvancedBlogPost(SimpleBlogPost): @transition(field="state", source="new", target="published") def publish(self): pass class FSMFieldTest(TestCase): def setUp(self): self.model = SimpleBlogPost() def test_initial_state_instantiated(self): assert self.model.state == "new" def test_known_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() assert self.model.state == "published" assert can_proceed(self.model.hide) self.model.hide() assert self.model.state == "hidden" def test_unknown_transition_fails(self): assert not can_proceed(self.model.hide) with pytest.raises(TransitionNotAllowed): self.model.hide() def test_state_non_changed_after_fail(self): assert can_proceed(self.model.remove) with pytest.raises(Exception, match="Upss"): self.model.remove() assert self.model.state == "new" def test_allowed_null_transition_should_succeed(self): self.model.publish() self.model.notify_all() assert self.model.state == "published" def test_unknown_null_transition_should_fail(self): with pytest.raises(TransitionNotAllowed): self.model.notify_all() assert self.model.state == "new" def test_multiple_source_support_path_1_works(self): self.model.publish() self.model.steal() assert self.model.state == "stolen" def test_multiple_source_support_path_2_works(self): self.model.publish() self.model.hide() self.model.steal() assert self.model.state == "stolen" def test_star_shortcut_succeed(self): assert can_proceed(self.model.moderate) self.model.moderate() assert self.model.state == "moderated" def test_plus_shortcut_succeeds_for_other_source(self): """Tests that the '+' shortcut succeeds for a source other than the target. """ assert can_proceed(self.model.block) self.model.block() assert self.model.state == "blocked" def test_plus_shortcut_fails_for_same_source(self): """Tests that the '+' shortcut fails if the source equals the target. """ self.model.block() assert not can_proceed(self.model.block) with pytest.raises(TransitionNotAllowed): self.model.block() def test_empty_string_target(self): self.model.empty() assert self.model.state == "" class StateSignalsTests(TestCase): def setUp(self): self.model = SimpleBlogPost() self.pre_transition_called = False self.post_transition_called = False pre_transition.connect(self.on_pre_transition, sender=SimpleBlogPost) post_transition.connect(self.on_post_transition, sender=SimpleBlogPost) def tearDown(self): pre_transition.disconnect(self.on_pre_transition, sender=SimpleBlogPost) post_transition.disconnect(self.on_post_transition, sender=SimpleBlogPost) def on_pre_transition(self, sender, instance, name, source, target, **kwargs): assert instance.state == source self.pre_transition_called = True def on_post_transition(self, sender, instance, name, source, target, **kwargs): assert instance.state == target self.post_transition_called = True def test_signals_called_on_valid_transition(self): self.model.publish() assert self.pre_transition_called assert self.post_transition_called def test_signals_not_called_on_invalid_transition(self): with pytest.raises(TransitionNotAllowed): self.model.hide() assert not self.pre_transition_called assert not self.post_transition_called class LazySenderTests(StateSignalsTests): def setUp(self): self.model = SimpleBlogPost() self.pre_transition_called = False self.post_transition_called = False pre_transition.connect(self.on_pre_transition, sender="testapp.SimpleBlogPost") post_transition.connect(self.on_post_transition, sender="testapp.SimpleBlogPost") def tearDown(self): pre_transition.disconnect(self.on_pre_transition, sender="testapp.SimpleBlogPost") post_transition.disconnect(self.on_post_transition, sender="testapp.SimpleBlogPost") class TestFieldTransitionsInspect(TestCase): def setUp(self): self.model = SimpleBlogPost() def test_transition_are_hashable(self) -> None: transition = Transition( method=self.model.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) assert hash(transition) is not None def test_transition_equality(self) -> None: for wrong_value in [0, 1, True, False, None]: assert ( Transition( method=AdvancedBlogPost.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) != wrong_value ) assert Transition( method=AdvancedBlogPost.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) != Transition( method=SimpleBlogPost.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) assert Transition( method=AdvancedBlogPost.empty, source="*", target="", on_error=None, conditions=[], permission=None, custom={}, ) == Transition( method=SimpleBlogPost.empty, source="*", target="", on_error=None, conditions=[], permission=None, custom={}, ) def test_in_operator_for_available_transitions(self): # store the generator in a list, so we can reuse the generator and do multiple asserts transitions = list(self.model.get_available_state_transitions()) # type: ignore[attr-defined] assert "publish" in transitions assert "xyz" not in transitions # inline method for faking the name of the transition def publish(): pass obj = Transition( method=publish, source="", target="", on_error="", conditions=None, permission="", custom=None, ) assert obj not in transitions def test_available_conditions_from_new(self): transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = { ("*", "moderated"), ("new", "published"), ("new", "removed"), ("*", ""), ("+", "blocked"), } assert actual == expected def test_available_conditions_from_published(self): self.model.publish() transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = { ("*", "moderated"), ("published", None), ("published", "hidden"), ("published", "stolen"), ("*", ""), ("+", "blocked"), } assert actual == expected def test_available_conditions_from_hidden(self): self.model.publish() self.model.hide() transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = {("*", "moderated"), ("hidden", "stolen"), ("*", ""), ("+", "blocked")} assert actual == expected def test_available_conditions_from_stolen(self): self.model.publish() self.model.steal() transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = {("*", "moderated"), ("*", ""), ("+", "blocked")} assert actual == expected def test_available_conditions_from_blocked(self): self.model.block() transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = {("*", "moderated"), ("*", "")} assert actual == expected def test_available_conditions_from_empty(self): self.model.empty() transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = {("*", "moderated"), ("*", ""), ("+", "blocked")} assert actual == expected def test_all_conditions(self): transitions = self.model.get_all_state_transitions() # type: ignore[attr-defined] actual = {(transition.source, transition.target) for transition in transitions} expected = { ("*", "moderated"), ("new", "published"), ("new", "removed"), ("published", None), ("published", "hidden"), ("published", "stolen"), ("hidden", "stolen"), ("*", ""), ("+", "blocked"), } assert actual == expected django-commons-django-fsm-2-10256af/tests/testapp/tests/test_conditions.py 0000664 0000000 0000000 00000003057 15153477071 0026722 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import TransitionNotAllowed from django_fsm import can_proceed from django_fsm import transition def condition_func(instance: models.Model) -> bool: return True class BlogPostWithConditions(models.Model): state = FSMField(default="new") def model_condition(self: models.Model) -> bool: return True def unmet_condition(self: models.Model) -> bool: return False @transition( field=state, source="new", target="published", conditions=[condition_func, model_condition] ) def publish(self): pass @transition( field=state, source="published", target="destroyed", conditions=[condition_func, unmet_condition], ) def destroy(self): pass class ConditionalTest(TestCase): def setUp(self): self.model = BlogPostWithConditions() def test_initial_staet(self): assert self.model.state == "new" def test_known_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() assert self.model.state == "published" def test_unmet_condition(self): self.model.publish() assert self.model.state == "published" assert not can_proceed(self.model.destroy) with pytest.raises(TransitionNotAllowed): self.model.destroy() assert can_proceed(self.model.destroy, check_conditions=False) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_custom_data.py 0000664 0000000 0000000 00000003037 15153477071 0027052 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class BlogPostWithCustomData(models.Model): state = FSMField(default="new") @transition( field=state, source="new", target="published", conditions=[], custom={"label": "Publish", "type": "*"}, ) def publish(self): pass @transition( field=state, source="published", target="destroyed", custom={"label": "Destroy", "type": "manual"}, ) def destroy(self): pass @transition( field=state, source="published", target="review", custom={"label": "Periodic review", "type": "automated"}, ) def review(self): pass class CustomTransitionDataTest(TestCase): def setUp(self): self.model = BlogPostWithCustomData() def test_initial_state(self): assert self.model.state == "new" transitions = list(self.model.get_available_state_transitions()) # type: ignore[attr-defined] assert len(transitions) == 1 assert transitions[0].target == "published" assert transitions[0].custom == {"label": "Publish", "type": "*"} def test_all_transitions_have_custom_data(self): transitions = self.model.get_all_state_transitions() # type: ignore[attr-defined] for t in transitions: assert t.custom["label"] is not None assert t.custom["type"] is not None django-commons-django-fsm-2-10256af/tests/testapp/tests/test_exception_transitions.py 0000664 0000000 0000000 00000003146 15153477071 0031203 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import can_proceed from django_fsm import transition from django_fsm.signals import post_transition class ExceptionalBlogPost(models.Model): state = FSMField(default="new") @transition(field=state, source="new", target="published", on_error="crashed") def publish(self): raise Exception("Upss") @transition(field=state, source="new", target="deleted") def delete(self): raise Exception("Upss") class FSMFieldExceptionTest(TestCase): def setUp(self): self.model = ExceptionalBlogPost() post_transition.connect(self.on_post_transition, sender=ExceptionalBlogPost) self.post_transition_data = {} def tearDown(self): post_transition.disconnect(self.on_post_transition, sender=ExceptionalBlogPost) def on_post_transition(self, **kwargs): self.post_transition_data = kwargs def test_state_changed_after_fail(self): assert can_proceed(self.model.publish) with pytest.raises(Exception, match="Upss"): self.model.publish() assert self.model.state == "crashed" assert self.post_transition_data["target"] == "crashed" assert "exception" in self.post_transition_data def test_state_not_changed_after_fail(self): assert can_proceed(self.model.delete) with pytest.raises(Exception, match="Upss"): self.model.delete() assert self.model.state == "new" assert self.post_transition_data == {} django-commons-django-fsm-2-10256af/tests/testapp/tests/test_graph_transitions.py 0000664 0000000 0000000 00000050711 15153477071 0030306 0 ustar 00root root 0000000 0000000 from __future__ import annotations import os import tempfile import typing from io import StringIO from pathlib import Path import graphviz import pytest from django.core.exceptions import FieldDoesNotExist from django.core.management import call_command from django.test import TestCase from django_fsm.management.commands.graph_transitions import node_label from django_fsm.management.commands.graph_transitions import node_name from tests.testapp.models import Application from tests.testapp.models import BlogPost from tests.testapp.models import BlogPostState from tests.testapp.tests.test_model_create_with_generic import Task from tests.testapp.tests.test_model_create_with_generic import TaskState class GraphTransitionsCommandTest(TestCase): MODELS_TO_TEST = [ "testapp.Application", "testapp.FKApplication", ] EXTENSIONS_TO_TEST = ["png", "jpg", "jpeg"] def test_node_name(self): assert node_name(Task.state.field, TaskState.DONE) == "testapp.task.state.done" assert node_name(BlogPost.state.field, BlogPostState.NEW) == "testapp.blog_post.state.0" def test_node_label(self): assert node_label(Application.state.field, "new") == "new" assert ( node_label(BlogPost.state.field, BlogPostState.PUBLISHED.value) == BlogPostState.PUBLISHED.label ) # choices is not declared, fallbacking to the value instead assert node_label(Task.state.field, TaskState.DONE.value) == TaskState.DONE.value def _call_command(self, *args: typing.Any, **kwargs: typing.Any) -> str: out = StringIO() call_command("graph_transitions", *args, **kwargs, stdout=out) return out.getvalue() def test_all_models(self): self._call_command() def test_app(self): self._call_command("testapp") def test_app_fail(self): with pytest.raises(LookupError): self._call_command("unknown_app") def test_single_model(self): for model in self.MODELS_TO_TEST: output = self._call_command(model) assert model in output for excluded_model in self.MODELS_TO_TEST: if model != excluded_model: assert excluded_model not in output def test_single_model_fail(self): with pytest.raises(LookupError): self._call_command("testapp.UnknownModel") def test_single_model_with_layouts(self): for model in self.MODELS_TO_TEST: for layout in graphviz.ENGINES: self._call_command("-l", layout, model) def test_single_model_with_output(self): with tempfile.TemporaryDirectory() as tmp_dir: previous_cwd = os.getcwd() try: # The command writes relative paths, so isolate it in a temp dir. os.chdir(tmp_dir) export_dir = Path("exports") export_dir.mkdir() for model in self.MODELS_TO_TEST: for extension in self.EXTENSIONS_TO_TEST: my_file = export_dir / f"{model}.{extension}" self._call_command("-o", my_file, model) assert my_file.exists() finally: os.chdir(previous_cwd) def test_single_model_exclude(self): excluded_transitions = ["standard", "no_target"] for model in self.MODELS_TO_TEST: output = self._call_command("-e", ",".join(excluded_transitions), model) for excluded_t in excluded_transitions: assert excluded_t not in output def test_single_field(self): """Test that specifying app.model.field filters to only that field.""" output = self._call_command("testapp.MultiStateApplication.another_state") assert "testapp.multi_state_application.another_state" in output assert "testapp.application.state" not in output def test_single_field_fail(self): with pytest.raises((LookupError, FieldDoesNotExist)): self._call_command("testapp.MultiStateApplication.unknown_field") with pytest.raises(LookupError): self._call_command("testapp.MultiStateApplication.id") def test_output_contains_subgraph_label(self): # noqa: PLR0915 output = self._call_command("testapp.Application") assert "subgraph cluster_testapp_Application_state {" in output assert 'graph [label="testapp.Application.state"]' in output assert '"testapp.application.state.new" [label=new shape=circle]' in output assert '"testapp.application.state._initial" [label="" shape=point]' in output assert '"testapp.application.state._initial" -> "testapp.application.state.new"' in output assert '"testapp.application.state.failed" [label=failed shape=circle]' in output assert '"testapp.application.state.None" [label=None shape=circle]' in output assert '"testapp.application.state.blocked" [label=blocked shape=circle]' in output assert '"testapp.application.state.hidden" [label=hidden shape=circle]' in output assert '"testapp.application.state.rejected" [label=rejected shape=circle]' in output assert '"testapp.application.state.moderated" [label=moderated shape=circle]' in output assert '"testapp.application.state.published" [label=published shape=circle]' in output assert ( '"testapp.application.state.new" -> "testapp.application.state.rejected" [label=get_state]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.published" [label=on_error]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.moderated" [label=return_value]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.blocked" [label=return_value]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.None" [label=no_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.failed" [style=dotted]' in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.published" [label=get_state]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.moderated" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.failed" [style=dotted]' in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.published" [label=standard]' # noqa: E501 in output ) assert ( '"testapp.application.state.hidden" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.blocked" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.new" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.rejected" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.published" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.rejected" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.published" [label=get_state_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.None" -> "testapp.application.state.rejected" [label=get_state_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.blocked" [label=any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.failed" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.moderated" -> "testapp.application.state.blocked" [label=return_value_any_source]' # noqa: E501 in output ) assert ( '"testapp.application.state.blocked" -> "testapp.application.state.moderated" [label=return_value_any_source_except_target]' # noqa: E501 in output ) assert ( '"testapp.application.state.published" -> "testapp.application.state.hidden" [label=any_source_except_target]' # noqa: E501 in output ) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_integer_field.py 0000664 0000000 0000000 00000002177 15153477071 0027353 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMIntegerField from django_fsm import TransitionNotAllowed from django_fsm import transition class BlogPostStateEnum: NEW = 10 PUBLISHED = 20 HIDDEN = 30 class BlogPostWithIntegerField(models.Model): state = FSMIntegerField(default=BlogPostStateEnum.NEW) @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED) def publish(self): pass @transition(field=state, source=BlogPostStateEnum.PUBLISHED, target=BlogPostStateEnum.HIDDEN) def hide(self): pass class BlogPostWithIntegerFieldTest(TestCase): def setUp(self): self.model = BlogPostWithIntegerField() def test_known_transition_should_succeed(self): self.model.publish() assert self.model.state == BlogPostStateEnum.PUBLISHED self.model.hide() assert self.model.state == BlogPostStateEnum.HIDDEN def test_unknown_transition_fails(self): with pytest.raises(TransitionNotAllowed): self.model.hide() django-commons-django-fsm-2-10256af/tests/testapp/tests/test_key_field.py 0000664 0000000 0000000 00000010256 15153477071 0026503 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMKeyField from django_fsm import TransitionNotAllowed from django_fsm import can_proceed from django_fsm import transition from tests.testapp.models import DbState FK_AVAILABLE_STATES = ( ("New", "_NEW_"), ("Published", "_PUBLISHED_"), ("Hidden", "_HIDDEN_"), ("Removed", "_REMOVED_"), ("Stolen", "_STOLEN_"), ("Moderated", "_MODERATED_"), ) class FKBlogPost(models.Model): state = FSMKeyField(DbState, default="new", protected=True, on_delete=models.CASCADE) @transition(field=state, source="new", target="published") def publish(self): pass @transition(field=state, source="published") def notify_all(self): pass @transition(field=state, source="published", target="hidden") def hide(self): pass @transition(field=state, source="new", target="removed") def remove(self): raise Exception("Upss") @transition(field=state, source=["published", "hidden"], target="stolen") def steal(self): pass @transition(field=state, source="*", target="moderated") def moderate(self): pass class FSMKeyFieldTest(TestCase): def setUp(self): DbState.objects.bulk_create( DbState(pk=item[0], label=item[1]) for item in FK_AVAILABLE_STATES ) self.model = FKBlogPost() def test_initial_state_instantiated(self): assert self.model.state == "new" def test_known_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() assert self.model.state == "published" assert can_proceed(self.model.hide) self.model.hide() assert self.model.state == "hidden" def test_unknown_transition_fails(self): assert not can_proceed(self.model.hide) with pytest.raises(TransitionNotAllowed): self.model.hide() def test_state_non_changed_after_fail(self): assert can_proceed(self.model.remove) with pytest.raises(Exception, match="Upss"): self.model.remove() assert self.model.state == "new" def test_allowed_null_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() self.model.notify_all() assert self.model.state == "published" def test_unknown_null_transition_should_fail(self): with pytest.raises(TransitionNotAllowed): self.model.notify_all() assert self.model.state == "new" def test_multiple_source_support_path_1_works(self): self.model.publish() self.model.steal() assert self.model.state == "stolen" def test_multiple_source_support_path_2_works(self): self.model.publish() self.model.hide() self.model.steal() assert self.model.state == "stolen" def test_star_shortcut_succeed(self): assert can_proceed(self.model.moderate) self.model.moderate() assert self.model.state == "moderated" """ # TODO: FIX it class BlogPostStatus(models.Model): name = models.CharField(unique=True, max_length=10) objects = models.Manager() class BlogPostWithFKState(models.Model): status = FSMKeyField(BlogPostStatus, default=lambda: BlogPostStatus.objects.get(name="new")) @transition(field=status, source='new', target='published') def publish(self): pass @transition(field=status, source='published', target='hidden') def hide(self): pass class BlogPostWithFKStateTest(TestCase): def setUp(self): BlogPostStatus.objects.bulk_create([ BlogPostStatus(name="new") BlogPostStatus(name="published") BlogPostStatus(name="hidden") ]) self.model = BlogPostWithFKState() def test_known_transition_should_succeed(self): self.model.publish() self.assertEqual(self.model.state, 'published') self.model.hide() self.assertEqual(self.model.state, 'hidden') def test_unknown_transition_fails(self): with pytest.raises(TransitionNotAllowed): self.model.hide() """ django-commons-django-fsm-2-10256af/tests/testapp/tests/test_lock_mixin.py 0000664 0000000 0000000 00000005610 15153477071 0026702 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import ConcurrentTransition from django_fsm import ConcurrentTransitionMixin from django_fsm import FSMField from django_fsm import transition class LockedBlogPost(ConcurrentTransitionMixin, models.Model): state = FSMField(default="new") text = models.CharField(max_length=50) objects: models.Manager[LockedBlogPost] = models.Manager() @transition(field=state, source="new", target="published") def publish(self): pass @transition(field=state, source="published", target="removed") def remove(self): pass class ExtendedBlogPost(LockedBlogPost): review_state = FSMField(default="waiting", protected=True) notes = models.CharField(max_length=50) objects: models.Manager[ExtendedBlogPost] = models.Manager() @transition(field=review_state, source="waiting", target="rejected") def reject(self): pass class TestLockMixin(TestCase): def test_create_succeed(self): LockedBlogPost.objects.create(text="test_create_succeed") def test_crud_succeed(self): post = LockedBlogPost(text="test_crud_succeed") post.publish() post.save() post = LockedBlogPost.objects.get(pk=post.pk) assert post.state == "published" post.text = "test_crud_succeed2" post.save() post = LockedBlogPost.objects.get(pk=post.pk) assert post.text == "test_crud_succeed2" post.delete() def test_save_and_change_succeed(self): post = LockedBlogPost(text="test_crud_succeed") post.publish() post.save() post.remove() post.save() post.delete() def test_concurrent_modifications_raise_exception(self): post1 = LockedBlogPost.objects.create() post2 = LockedBlogPost.objects.get(pk=post1.pk) post1.publish() post1.save() post2.text = "aaa" post2.publish() with pytest.raises(ConcurrentTransition): post2.save() def test_inheritance_crud_succeed(self): post = ExtendedBlogPost(text="test_inheritance_crud_succeed", notes="reject me") post.publish() post.save() post = ExtendedBlogPost.objects.get(pk=post.pk) assert post.state == "published" post.text = "test_inheritance_crud_succeed2" post.reject() post.save() post = ExtendedBlogPost.objects.get(pk=post.pk) assert post.review_state == "rejected" assert post.text == "test_inheritance_crud_succeed2" def test_concurrent_modifications_after_refresh_db_succeed(self): # bug 255 post1 = LockedBlogPost.objects.create() post2 = LockedBlogPost.objects.get(pk=post1.pk) post1.publish() post1.save() post2.refresh_from_db() post2.remove() post2.save() django-commons-django-fsm-2-10256af/tests/testapp/tests/test_mixin_support.py 0000664 0000000 0000000 00000001275 15153477071 0027471 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class WorkflowMixin: @transition(field="state", source="*", target="draft") def draft(self): pass @transition(field="state", source="draft", target="published") def publish(self): pass class MixinSupportTestModel(WorkflowMixin, models.Model): state = FSMField(default="new") class Test(TestCase): def test_usecase(self): model = MixinSupportTestModel() model.draft() assert model.state == "draft" model.publish() assert model.state == "published" django-commons-django-fsm-2-10256af/tests/testapp/tests/test_model_create_with_generic.py 0000664 0000000 0000000 00000002261 15153477071 0031717 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class Ticket(models.Model): objects: models.Manager[Ticket] = models.Manager() class TaskState(models.TextChoices): NEW = "new", "New" DONE = "done", "Done" class Task(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() causality = GenericForeignKey("content_type", "object_id") state = FSMField(default=TaskState.NEW) objects: models.Manager[Task] = models.Manager() @transition(field=state, source=TaskState.NEW, target=TaskState.DONE) def do(self): pass class Test(TestCase): def setUp(self): self.ticket = Ticket.objects.create() def test_model_objects_create(self): """Check a model with state field can be created if one of the other fields is a property or a virtual field. """ Task.objects.create(causality=self.ticket) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_multi_resultstate.py 0000664 0000000 0000000 00000004741 15153477071 0030343 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import GET_STATE from django_fsm import RETURN_VALUE from django_fsm import FSMField from django_fsm import transition from django_fsm.signals import post_transition from django_fsm.signals import pre_transition class MultiResultTest(models.Model): state = FSMField(default="new") @transition(field=state, source="new", target=RETURN_VALUE("for_moderators", "published")) def publish(self, *, is_public=False): return "published" if is_public else "for_moderators" @transition( field=state, source="for_moderators", target=GET_STATE( lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"], ), ) def moderate(self, allowed): pass class Test(TestCase): def test_return_state_succeed(self): instance = MultiResultTest() instance.publish(is_public=True) assert instance.state == "published" def test_get_state_succeed(self): instance = MultiResultTest(state="for_moderators") instance.moderate(allowed=False) assert instance.state == "rejected" class TestSignals(TestCase): def setUp(self): self.pre_transition_called = False self.post_transition_called = False pre_transition.connect(self.on_pre_transition, sender=MultiResultTest) post_transition.connect(self.on_post_transition, sender=MultiResultTest) def tearDown(self): pre_transition.disconnect(self.on_pre_transition, sender=MultiResultTest) post_transition.disconnect(self.on_post_transition, sender=MultiResultTest) def on_pre_transition(self, sender, instance, name, source, target, **kwargs): assert instance.state == source self.pre_transition_called = True def on_post_transition(self, sender, instance, name, source, target, **kwargs): assert instance.state == target self.post_transition_called = True def test_signals_called_with_get_state(self): instance = MultiResultTest(state="for_moderators") instance.moderate(allowed=False) assert self.pre_transition_called assert self.post_transition_called def test_signals_called_with_return_value(self): instance = MultiResultTest() instance.publish(is_public=True) assert self.pre_transition_called assert self.post_transition_called django-commons-django-fsm-2-10256af/tests/testapp/tests/test_multidecorators.py 0000664 0000000 0000000 00000002101 15153477071 0027756 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition from django_fsm.signals import post_transition class MultiDecoratedModel(models.Model): counter = models.IntegerField(default=0) signal_counter = models.IntegerField(default=0) state = FSMField(default="SUBMITTED_BY_USER") @transition(field=state, source="SUBMITTED_BY_USER", target="REVIEW_USER") @transition(field=state, source="SUBMITTED_BY_ADMIN", target="REVIEW_ADMIN") @transition(field=state, source="SUBMITTED_BY_ANONYMOUS", target="REVIEW_ANONYMOUS") def review(self): self.counter += 1 def count_calls(sender, instance, name, source, target, **kwargs): instance.signal_counter += 1 post_transition.connect(count_calls, sender=MultiDecoratedModel) class TestStateProxy(TestCase): def test_transition_method_called_once(self): model = MultiDecoratedModel() model.review() assert model.counter == 1 assert model.signal_counter == 1 django-commons-django-fsm-2-10256af/tests/testapp/tests/test_object_permissions.py 0000664 0000000 0000000 00000003306 15153477071 0030447 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.contrib.auth.models import User from django.db import models from django.test import TestCase from django.test.utils import override_settings from guardian.shortcuts import assign_perm from django_fsm import FSMField from django_fsm import has_transition_perm from django_fsm import transition class ObjectPermissionTestModel(models.Model): state = FSMField(default="new") objects: models.Manager[ObjectPermissionTestModel] = models.Manager() class Meta: permissions = [ ("can_publish_objectpermissiontestmodel", "Can publish ObjectPermissionTestModel"), ] @transition( field=state, source="new", target="published", on_error="failed", permission="testapp.can_publish_objectpermissiontestmodel", ) def publish(self) -> None: pass @override_settings( AUTHENTICATION_BACKENDS=( "django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend", ) ) class ObjectPermissionFSMFieldTest(TestCase): def setUp(self): super().setUp() self.model = ObjectPermissionTestModel.objects.create() self.unprivileged = User.objects.create(username="unprivileged") self.privileged = User.objects.create(username="object_only_privileged") assign_perm("can_publish_objectpermissiontestmodel", self.privileged, self.model) def test_object_only_access_success(self): assert has_transition_perm(self.model.publish, self.privileged) self.model.publish() def test_object_only_other_access_prohibited(self): assert not has_transition_perm(self.model.publish, self.unprivileged) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_permissions.py 0000664 0000000 0000000 00000003511 15153477071 0027117 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.test import TestCase from django_fsm import has_transition_perm from tests.testapp.models import BlogPost class PermissionFSMFieldTest(TestCase): def setUp(self): self.model = BlogPost() self.unprivileged = User.objects.create(username="unprivileged") self.privileged = User.objects.create(username="privileged") self.staff = User.objects.create(username="staff", is_staff=True) self.privileged.user_permissions.add( Permission.objects.get_by_natural_key("can_publish_post", "testapp", "blogpost") ) self.privileged.user_permissions.add( Permission.objects.get_by_natural_key("can_remove_post", "testapp", "blogpost") ) def test_privileged_access_succeed(self): assert has_transition_perm(self.model.publish, self.privileged) assert has_transition_perm(self.model.remove, self.privileged) transitions = self.model.get_available_user_state_transitions(self.privileged) # type: ignore[attr-defined] assert {"publish", "remove", "moderate"} == {transition.name for transition in transitions} def test_unprivileged_access_prohibited(self): assert not has_transition_perm(self.model.publish, self.unprivileged) assert not has_transition_perm(self.model.remove, self.unprivileged) transitions = self.model.get_available_user_state_transitions(self.unprivileged) # type: ignore[attr-defined] assert {"moderate"} == {transition.name for transition in transitions} def test_permission_instance_method(self): assert not has_transition_perm(self.model.restore, self.unprivileged) assert has_transition_perm(self.model.restore, self.staff) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_protected_field.py 0000664 0000000 0000000 00000002355 15153477071 0027705 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class ProtectedAccessModel(models.Model): status = FSMField(default="new", protected=True) objects: models.Manager[ProtectedAccessModel] = models.Manager() @transition(field=status, source="new", target="published") def publish(self): pass class MultiProtectedAccessModel(models.Model): status1 = FSMField(default="new", protected=True) status2 = FSMField(default="new", protected=True) objects: models.Manager[MultiProtectedAccessModel] = models.Manager() class TestDirectAccessModels(TestCase): def test_multi_protected_field_create(self): obj = MultiProtectedAccessModel.objects.create() assert obj.status1 == "new" assert obj.status2 == "new" def test_no_direct_access(self): instance = ProtectedAccessModel() assert instance.status == "new" def try_change() -> None: instance.status = "change" with pytest.raises(AttributeError): try_change() instance.publish() instance.save() assert instance.status == "published" django-commons-django-fsm-2-10256af/tests/testapp/tests/test_protected_fields.py 0000664 0000000 0000000 00000003346 15153477071 0030071 0 ustar 00root root 0000000 0000000 from __future__ import annotations import pytest from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import FSMModelMixin from django_fsm import transition class RefreshableProtectedAccessModel(models.Model): status = FSMField(default="new", protected=True) objects: models.Manager[RefreshableProtectedAccessModel] = models.Manager() @transition(field=status, source="new", target="published") def publish(self): pass class RefreshableModel(FSMModelMixin, RefreshableProtectedAccessModel): pass class TestDirectAccessModels(TestCase): def test_no_direct_access(self): instance = RefreshableProtectedAccessModel() assert instance.status == "new" with pytest.raises(AttributeError): instance.status = "change" instance.publish() instance.save() assert instance.status == "published" def test_refresh_from_db(self): instance = RefreshableModel() assert instance.status == "new" instance.save() instance.refresh_from_db() assert instance.status == "new" def test_concurrent_refresh_from_db(self): instance = RefreshableModel() assert instance.status == "new" instance.save() # NOTE: This simulates a concurrent update scenario concurrent_instance = RefreshableModel.objects.get(pk=instance.pk) assert concurrent_instance.status == instance.status == "new" concurrent_instance.publish() assert concurrent_instance.status == "published" concurrent_instance.save() assert instance.status == "new" instance.refresh_from_db() assert instance.status == "published" django-commons-django-fsm-2-10256af/tests/testapp/tests/test_proxy_inheritance.py 0000664 0000000 0000000 00000003332 15153477071 0030277 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import can_proceed from django_fsm import transition class BaseModel(models.Model): state = FSMField(default="new") @transition(field=state, source="new", target="published") def publish(self): pass class InheritedModel(BaseModel): class Meta: proxy = True @transition(field="state", source="published", target="sticked") def stick(self): pass class TestinheritedModel(TestCase): def setUp(self): self.model = InheritedModel() def test_known_transition_should_succeed(self): assert can_proceed(self.model.publish) self.model.publish() assert self.model.state == "published" assert can_proceed(self.model.stick) self.model.stick() assert self.model.state == "sticked" def test_field_available_transitions_works(self): self.model.publish() assert self.model.state == "published" transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined] assert [data.target for data in transitions] == ["sticked"] def test_field_all_transitions_base_model(self): transitions = BaseModel().get_all_state_transitions() # type: ignore[attr-defined] assert {("new", "published")} == {(data.source, data.target) for data in transitions} def test_field_all_transitions_works(self): transitions = self.model.get_all_state_transitions() # type: ignore[attr-defined] assert {("new", "published"), ("published", "sticked")} == { (data.source, data.target) for data in transitions } django-commons-django-fsm-2-10256af/tests/testapp/tests/test_state_transitions.py 0000664 0000000 0000000 00000003174 15153477071 0030326 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class Insect(models.Model): class STATE: CATERPILLAR = "CTR" BUTTERFLY = "BTF" STATE_CHOICES = ( (STATE.CATERPILLAR, "Caterpillar", "Caterpillar"), (STATE.BUTTERFLY, "Butterfly", "Butterfly"), ) state = FSMField(default=STATE.CATERPILLAR, state_choices=STATE_CHOICES) objects: models.Manager[Insect] = models.Manager() @transition(field=state, source=STATE.CATERPILLAR, target=STATE.BUTTERFLY) def cocoon(self): pass def fly(self): raise NotImplementedError def crawl(self): raise NotImplementedError class Caterpillar(Insect): class Meta: proxy = True def crawl(self): """ Do crawl """ class Butterfly(Insect): class Meta: proxy = True def fly(self): """ Do fly """ class TestStateProxy(TestCase): def test_initial_proxy_set_succeed(self): insect = Insect() assert isinstance(insect, Caterpillar) def test_transition_proxy_set_succeed(self): insect = Insect() insect.cocoon() assert isinstance(insect, Butterfly) def test_load_proxy_set(self): Insect.objects.bulk_create( [ Insect(state=Insect.STATE.CATERPILLAR), Insect(state=Insect.STATE.BUTTERFLY), ] ) insects = Insect.objects.all() assert {Caterpillar, Butterfly} == {insect.__class__ for insect in insects} django-commons-django-fsm-2-10256af/tests/testapp/tests/test_string_field_parameter.py 0000664 0000000 0000000 00000001504 15153477071 0031255 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import transition class BlogPostWithStringField(models.Model): state = FSMField(default="new") @transition(field="state", source="new", target="published", conditions=[]) def publish(self): pass @transition(field="state", source="published", target="destroyed") def destroy(self): pass @transition(field="state", source="published", target="review") def review(self): pass class StringFieldTestCase(TestCase): def setUp(self): self.model = BlogPostWithStringField() def test_initial_state(self): assert self.model.state == "new" self.model.publish() assert self.model.state == "published" django-commons-django-fsm-2-10256af/tests/testapp/tests/test_transition_all_except_target.py 0000664 0000000 0000000 00000001476 15153477071 0032514 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django.test import TestCase from django_fsm import FSMField from django_fsm import can_proceed from django_fsm import transition class ExceptTargetTransition(models.Model): state = FSMField(default="new") @transition(field=state, source="new", target="published") def publish(self): pass @transition(field=state, source="+", target="removed") def remove(self): pass class TestExceptTargetTransition(TestCase): def setUp(self): self.model = ExceptTargetTransition() def test_usecase(self): assert self.model.state == "new" assert can_proceed(self.model.remove) self.model.remove() assert self.model.state == "removed" assert not can_proceed(self.model.remove) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_transition_hash_eq.py 0000664 0000000 0000000 00000003075 15153477071 0030433 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.db import models from django_fsm import Transition def test_transition_eq_matches_name_and_transition() -> None: def publish() -> None: pass transition = Transition( method=publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) def other() -> None: pass other.__name__ = "publish" other_transition = Transition( method=other, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) assert transition == "publish" assert transition != other_transition assert transition != "other" assert transition != object() def test_transition_same_name_different_models_not_equal() -> None: class First(models.Model): def publish(self) -> None: pass class Second(models.Model): def publish(self) -> None: pass first_transition = Transition( method=First.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) second_transition = Transition( method=Second.publish, source="new", target="published", on_error=None, conditions=[], permission=None, custom={}, ) assert first_transition != second_transition assert hash(first_transition) != hash(second_transition) django-commons-django-fsm-2-10256af/tests/testapp/tests/test_typing.py 0000664 0000000 0000000 00000003542 15153477071 0026062 0 ustar 00root root 0000000 0000000 from __future__ import annotations import os import subprocess import sys from pathlib import Path from django.test import TestCase class BaseAdminTestCase(TestCase): project_root: Path env: dict[str, str] def setUp(self) -> None: self.project_root = Path(__file__).resolve().parents[3] self.env = os.environ.copy() self.env.pop("DJANGO_SETTINGS_MODULE", None) python_path = self.env.get("PYTHONPATH") self.env["PYTHONPATH"] = ( f"{self.project_root}{os.pathsep}{python_path}" if python_path else str(self.project_root) ) def test_admin_module_imports_without_django_stubs_monkeypatch(self) -> None: result = subprocess.run( # noqa: S603 [ sys.executable, "-c", ( "from django.conf import settings; " "settings.configure(SECRET_KEY='test', USE_I18N=False); " "import django_fsm.admin" ), ], capture_output=True, check=False, cwd=self.project_root, env=self.env, text=True, ) assert result.returncode == 0, result.stderr def test_main_module_imports_without_django_stubs_monkeypatch(self) -> None: result = subprocess.run( # noqa: S603 [ sys.executable, "-c", ( "from django.conf import settings; " "settings.configure(SECRET_KEY='test', USE_I18N=False); " "import django_fsm" ), ], capture_output=True, check=False, cwd=self.project_root, env=self.env, text=True, ) assert result.returncode == 0, result.stderr django-commons-django-fsm-2-10256af/tests/urls.py 0000664 0000000 0000000 00000000250 15153477071 0021645 0 ustar 00root root 0000000 0000000 from __future__ import annotations from django.contrib import admin from django.urls import path urlpatterns = [ path("admin/", admin.site.urls, name="admin"), ] django-commons-django-fsm-2-10256af/tests/wsgi.py 0000664 0000000 0000000 00000000646 15153477071 0021642 0 ustar 00root root 0000000 0000000 """WSGI config for silvr project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ """ from __future__ import annotations import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") application = get_wsgi_application() django-commons-django-fsm-2-10256af/tox.ini 0000664 0000000 0000000 00000001222 15153477071 0020457 0 ustar 00root root 0000000 0000000 [tox] envlist = py{310,311}-dj42 py{310,311,312}-dj50 py{310,311,312}-dj51 py{310,311,312,313,314}-dj52 py{312,313,314}-dj60 py{312,313,314}-djmain skipsdist = True [testenv] deps = dj42: Django==4.2 dj50: Django==5.0 dj51: Django==5.1 dj52: Django==5.2 dj60: Django==6.0 djmain: https://github.com/django/django/tarball/main django-fsm-log django-guardian django-stubs-ext graphviz pep8 pyflakes pytest pytest-django pytest-cov commands = {posargs:python -m pytest} [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 django-commons-django-fsm-2-10256af/uv.lock 0000664 0000000 0000000 00000346200 15153477071 0020460 0 ustar 00root root 0000000 0000000 version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version < '3.12'", ] [[package]] name = "asgiref" version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] [[package]] name = "cachetools" version = "7.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "coverage" version = "7.13.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "django" version = "5.2.11" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ { name = "asgiref", marker = "python_full_version < '3.12'" }, { name = "sqlparse", marker = "python_full_version < '3.12'" }, { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" }, ] [[package]] name = "django" version = "6.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ { name = "asgiref", marker = "python_full_version >= '3.12'" }, { name = "sqlparse", marker = "python_full_version >= '3.12'" }, { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/3e/a1c4207c5dea4697b7a3387e26584919ba987d8f9320f59dc0b5c557a4eb/django-6.0.2.tar.gz", hash = "sha256:3046a53b0e40d4b676c3b774c73411d7184ae2745fe8ce5e45c0f33d3ddb71a7", size = 10886874, upload-time = "2026-02-03T13:50:31.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" }, ] [[package]] name = "django-appconf" version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/a2/e58bec8d7941b914af52a67c35b5709eceed2caa2848f28437f1666ed668/django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec", size = 16127, upload-time = "2025-11-08T15:46:27.304Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/e6/4c34d94dfb74bbcbc489606e61f1924933de30d22c593dd1f429f35fbd7f/django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4", size = 6500, upload-time = "2025-11-08T15:46:25.957Z" }, ] [[package]] name = "django-fsm-2" version = "4.2.1" source = { editable = "." } dependencies = [ { name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "django-stubs-ext" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] [package.optional-dependencies] graphviz = [ { name = "graphviz" }, ] [package.dev-dependencies] dev = [ { name = "coverage" }, { name = "django-fsm-log" }, { name = "django-guardian" }, { name = "graphviz" }, { name = "mypy" }, { name = "prek" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-randomly" }, { name = "tox" }, ] [package.metadata] requires-dist = [ { name = "django", specifier = ">=4.2.29" }, { name = "django-stubs-ext" }, { name = "graphviz", marker = "extra == 'graphviz'" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] provides-extras = ["graphviz"] [package.metadata.requires-dev] dev = [ { name = "coverage" }, { name = "django-fsm-log" }, { name = "django-guardian" }, { name = "graphviz" }, { name = "mypy" }, { name = "prek" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-randomly" }, { name = "tox" }, ] [[package]] name = "django-fsm-log" version = "5.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "django-appconf" }, { name = "django-fsm-2" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/5f/f59917a2e0abeaf9cf20b7920c3df966c813754c61cb3d6587ace30d5e28/django_fsm_log-5.0.2.tar.gz", hash = "sha256:5276a587bff3112c123c73456f37decf9c1bdc631e005c760e248adc25c8ec97", size = 8758, upload-time = "2026-01-27T11:08:49.532Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/ca/79414e6548d0c2585c7d134250152f12071b6a270367a693e6ea704be011/django_fsm_log-5.0.2-py3-none-any.whl", hash = "sha256:02fbb61a54b21c1640e6360a251e3a07b564307bbdd391bbbf65ca8299f13d9e", size = 13964, upload-time = "2026-01-27T11:08:48.056Z" }, ] [[package]] name = "django-guardian" version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/eb/bbeb4efd10d6cca8993697f571f17574e9fa7a912cead3ab39ce1d3793cd/django_guardian-3.3.0.tar.gz", hash = "sha256:abf1487399212cffdce7b3c909182a26fbe7e89746007299a8cab99f3d5ff009", size = 107443, upload-time = "2026-02-24T19:43:28.819Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/3c/6517c5e27c6f9c165f989a5884f8798d66d25ce86fe44bf8c19aa4120351/django_guardian-3.3.0-py3-none-any.whl", hash = "sha256:4dca4fce104c7306e41b947a57d1cd6be46d9982548bef194ac8a6ad61d83686", size = 144003, upload-time = "2026-02-24T19:43:27Z" }, ] [[package]] name = "django-stubs-ext" version = "5.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django", version = "5.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "django", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/03/9c2be939490d2282328db4611bc5956899f5ff7eabc3e88bd4b964a87373/django_stubs_ext-5.2.9.tar.gz", hash = "sha256:6db4054d1580657b979b7d391474719f1a978773e66c7070a5e246cd445a25a9", size = 6497, upload-time = "2026-01-20T23:58:59.462Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/0d5f7d7e76fe972d9f560f687fdc0cab4db9e1624fd90728ca29b4ed7a63/django_stubs_ext-5.2.9-py3-none-any.whl", hash = "sha256:230c51575551b0165be40177f0f6805f1e3ebf799b835c85f5d64c371ca6cf71", size = 9974, upload-time = "2026-01-20T23:58:58.438Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] 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" } wheels = [ { 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" }, ] [[package]] name = "filelock" version = "3.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, ] [[package]] name = "graphviz" version = "0.21" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "librt" version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "platformdirs" version = "4.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "prek" version = "0.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c6/51/2324eaad93a4b144853ca1c56da76f357d3a70c7b4fd6659e972d7bb8660/prek-0.3.4.tar.gz", hash = "sha256:56a74d02d8b7dfe3c774ecfcd8c1b4e5f1e1b84369043a8003e8e3a779fce72d", size = 356633, upload-time = "2026-02-28T03:47:13.452Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/09/20/1a964cb72582307c2f1dc7f583caab90f42810ad41551e5220592406a4c3/prek-0.3.4-py3-none-linux_armv6l.whl", hash = "sha256:c35192d6e23fe7406bd2f333d1c7dab1a4b34ab9289789f453170f33550aa74d", size = 4641915, upload-time = "2026-02-28T03:47:03.772Z" }, { url = "https://files.pythonhosted.org/packages/c5/cb/4a21f37102bac37e415b61818344aa85de8d29a581253afa7db8c08d5a33/prek-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f784d78de72a8bbe58a5fe7bde787c364ae88f0aff5222c5c5c7287876c510a", size = 4649166, upload-time = "2026-02-28T03:47:06.164Z" }, { url = "https://files.pythonhosted.org/packages/85/9c/a7c0d117a098d57931428bdb60fcb796e0ebc0478c59288017a2e22eca96/prek-0.3.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50a43f522625e8c968e8c9992accf9e29017abad6c782d6d176b73145ad680b7", size = 4274422, upload-time = "2026-02-28T03:46:59.356Z" }, { url = "https://files.pythonhosted.org/packages/59/84/81d06df1724d09266df97599a02543d82fde7dfaefd192f09d9b2ccb092f/prek-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4bbb1d3912a88935f35c6ba4466b4242732e3e3a8c608623c708e83cea85de00", size = 4629873, upload-time = "2026-02-28T03:46:56.419Z" }, { url = "https://files.pythonhosted.org/packages/09/cd/bb0aefa25cfacd8dbced75b9a9d9945707707867fa5635fb69ae1bbc2d88/prek-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca4d4134db8f6e8de3c418317becdf428957e3cab271807f475318105fd46d04", size = 4552507, upload-time = "2026-02-28T03:47:05.004Z" }, { url = "https://files.pythonhosted.org/packages/9b/c0/578a7af4861afb64ec81c03bfdcc1bb3341bb61f2fff8a094ecf13987a56/prek-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb6395f6eb76133bb1e11fc718db8144522466cdc2e541d05e7813d1bbcae7d", size = 4865929, upload-time = "2026-02-28T03:47:09.231Z" }, { url = "https://files.pythonhosted.org/packages/fc/48/f169406590028f7698ef2e1ff5bffd92ca05e017636c1163a2f5ef0f8275/prek-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae17813239ddcb4ae7b38418de4d49afff740f48f8e0556029c96f58e350412", size = 5390286, upload-time = "2026-02-28T03:47:10.796Z" }, { url = "https://files.pythonhosted.org/packages/05/c5/98a73fec052059c3ae06ce105bef67caca42334c56d84e9ef75df72ba152/prek-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a621a690d9c127afc3d21c275030d364d1fbef3296c095068d3ae80a59546e", size = 4891028, upload-time = "2026-02-28T03:47:07.916Z" }, { url = "https://files.pythonhosted.org/packages/a3/b4/029966e35e59b59c142be7e1d2208ad261709ac1a66aa4a3ce33c5b9f91f/prek-0.3.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d978c31bc3b1f0b3d58895b7c6ac26f077e0ea846da54f46aeee4c7088b1b105", size = 4633986, upload-time = "2026-02-28T03:47:14.351Z" }, { url = "https://files.pythonhosted.org/packages/1d/27/d122802555745b6940c99fcb41496001c192ddcdf56ec947ec10a0298e05/prek-0.3.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8e089a030f0a023c22a4bb2ec4ff3fcc153585d701cff67acbfca2f37e173ae", size = 4680722, upload-time = "2026-02-28T03:47:12.224Z" }, { url = "https://files.pythonhosted.org/packages/34/40/92318c96b3a67b4e62ed82741016ede34d97ea9579d3cc1332b167632222/prek-0.3.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8060c72b764f0b88112616763da9dd3a7c293e010f8520b74079893096160a2f", size = 4535623, upload-time = "2026-02-28T03:46:52.221Z" }, { url = "https://files.pythonhosted.org/packages/df/f5/6b383d94e722637da4926b4f609d36fe432827bb6f035ad46ee02bde66b6/prek-0.3.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:65b23268456b5a763278d4e1ec532f2df33918f13ded85869a1ddff761eb9697", size = 4729879, upload-time = "2026-02-28T03:46:57.886Z" }, { url = "https://files.pythonhosted.org/packages/79/f8/fdc705b807d813fd713ffa4f67f96741542ed1dafbb221206078c06f3df4/prek-0.3.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3975c61139c7b3200e38dc3955e050b0f2615701d3deb9715696a902e850509e", size = 5001569, upload-time = "2026-02-28T03:47:00.892Z" }, { url = "https://files.pythonhosted.org/packages/84/92/b007a41f58e8192a1e611a21b396ad870d51d7873b7af12068ebae7fc15f/prek-0.3.4-py3-none-win32.whl", hash = "sha256:37449ae82f4dc08b72e542401e3d7318f05d1163e87c31ab260a40f425d6516e", size = 4297057, upload-time = "2026-02-28T03:47:02.219Z" }, { url = "https://files.pythonhosted.org/packages/bb/dc/bcb02de9b11461e8e0c7d3c8fdf8cfa15ac6efe73472a4375549ba5defd2/prek-0.3.4-py3-none-win_amd64.whl", hash = "sha256:60e9aa86ca65de963510ae28c5d94b9d7a97bcbaa6e4cdb5bf5083ed4c45dc71", size = 4655174, upload-time = "2026-02-28T03:46:53.749Z" }, { url = "https://files.pythonhosted.org/packages/0b/86/98f5598569f4cd3de7161e266fab6a8981e65555f79d4704810c1502ad0a/prek-0.3.4-py3-none-win_arm64.whl", hash = "sha256:486bdae8f4512d3b4f6eb61b83e5b7595da2adca385af4b2b7823c0ab38d1827", size = 4367817, upload-time = "2026-02-28T03:46:55.264Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "pyproject-api" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] 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" } wheels = [ { 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" }, ] [[package]] name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-django" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, ] [[package]] name = "pytest-randomly" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] 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" } wheels = [ { 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" }, ] [[package]] name = "python-discovery" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, ] [[package]] name = "sqlparse" version = "0.5.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tox" version = "4.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/4a/6ea2602afe685f842ff9b5e07196e693f1aba885164d171af4807075cb30/tox-4.47.0.tar.gz", hash = "sha256:db08368214f6f44b3e9b6c6e937140e25a4b0cea63f8489bf1c9b6b34d3e42e3", size = 253965, upload-time = "2026-03-01T15:00:05.563Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/95/727c8cde3ef125706825242833c41d0293307983cb9de7d1ad6dda503bfa/tox-4.47.0-py3-none-any.whl", hash = "sha256:79260c47814086313eea516c6cd4ce374f93c19be2de6125e7d330356a000364", size = 202065, upload-time = "2026-03-01T15:00:03.808Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ { 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" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "virtualenv" version = "21.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, ]