Add polymorphic guard to solve deletion problems.

Includes docs and robust tests.
This commit is contained in:
Brian Kohan 2025-12-17 14:36:09 -08:00 committed by Brian Kohan
parent c3136da180
commit 39a27e8c7c
No known key found for this signature in database
GPG Key ID: 5C6CE8BA38C43FC1
25 changed files with 3253 additions and 194 deletions

3
.gitignore vendored
View File

@ -61,6 +61,9 @@ local_settings.py
db.sqlite3
db.sqlite3-journal
# Test migrations (generated dynamically by tests)
src/polymorphic/tests/test_migrations/migrations/0*.py
# Flask stuff:
instance/
.webassets-cache

View File

@ -16,6 +16,7 @@ API Documentation
polymorphic.formsets
polymorphic.managers
polymorphic.models
polymorphic.deletion
polymorphic.query
polymorphic.showfields
polymorphic.templatetags

View File

@ -0,0 +1,12 @@
polymorphic.models
==================
.. automodule:: polymorphic.deletion
.. autoclass:: polymorphic.deletion.PolymorphicGuard
:members: __call__
:show-inheritance:
.. autoclass:: polymorphic.deletion.PolymorphicGuardSerializer
:members:
:show-inheritance:

51
docs/deletion.rst Normal file
View File

@ -0,0 +1,51 @@
Deletion
========
.. versionadded:: 4.5.0
There is nothing special about deleting polymorphic models. The same rules apply as to
:ref:`the deletion of normal Django models <topics-db-queries-delete>` that have parent/child
relationships up and down a model inheritance hierarchy. Django must walk the model inheritance and
relationship graph and collect all of the affected objects so that it can correctly order deletion
SQL statements to respect database constraints and issue signals.
The polymorphic deletion logic is the same as the normal Django deletion logic because Django
already walks the model inheritance hierarchy. :class:`~polymorphic.query.PolymorphicQuerySet` and
:class:`~polymorphic.managers.PolymorphicManager` disrupt this process by confusing Django's graph
walker by returning concrete subclass instances instead of base class instances when it attempts to
walk reverse relationships to polymorphic models. To prevent this confusion,
:pypi:`django-polymorphic` wraps the :attr:`~django.db.models.ForeignKey.on_delete` handlers of
reverse relations to polymorphic models with :class:`~polymorphic.deletion.PolymorphicGuard`
which disables polymorphic behavior on the related querysets during collection.
**You may define your polymorphic models as you normally would using the standard Django**
:attr:`~django.db.models.ForeignKey.on_delete` **actions**.
:class:`~polymorphic.models.PolymorphicModel` will automatically wrap the actions for you. actions
wrapped with :class:`~polymorphic.deletion.PolymorphicGuard` serialize in migrations as the
underlying wrapped action. This ensures migrations generated by versions of
:pypi:`django-polymorphic` after 4.5.0 should be the same as with prior versions. The guard is also
unnecessary during migrations because Django generates basic managers instead of using the default
polymorphic managers.
It is a design goal of :pypi:`django-polymorphic` that deletion should just work without any special
treatment. However if you encounter attribute errors or database integrity errors during deletion
you may manually wrap the :attr:`~django.db.models.ForeignKey.on_delete` action of reverse relations
to polymorphic models with :class:`~polymorphic.deletion.PolymorphicGuard` to disable polymorphic
behavior during deletion collection. If you encounter an issue like this
`please report it to us <https://github.com/jazzband/django-polymorphic/issues>`_. For example:
.. code-block:: python
from polymorphic.models import PolymorphicModel
from polymorphic.deletion import PolymorphicGuard
from django.db import models
class MyModel(models.Model):
# ...
class RelatedModel(PolymorphicModel):
my_model = models.ForeignKey(
MyModel,
on_delete=PolymorphicGuard(models.CASCADE),
)

View File

@ -119,6 +119,7 @@ Advanced topics
formsets
migrating
managers
deletion
advanced
changelog
api/index

View File

@ -97,6 +97,7 @@ build: build-docs-html
# regenerate test migrations using the lowest version of Django
make-test-migrations:
- rm src/polymorphic/tests/migrations/00*.py
- rm src/polymorphic/tests/deletion/migrations/00*.py
uv run --isolated --resolution lowest-direct --script ./manage.py makemigrations
# open the html documentation

View File

@ -143,6 +143,7 @@ show_missing = true
dev = [
"coverage>=7.6.1",
"dj-database-url>=2.2.0",
"django-test-migrations>=1.5.0",
"ipdb>=0.13.13",
"ipython>=8.18.1",
"mypy>=1.14.1",

View File

@ -10,6 +10,7 @@ import warnings
from django.db import models
from django.db.models.base import ModelBase
from .deletion import PolymorphicGuard
from .managers import PolymorphicManager
from .query import PolymorphicQuerySet
@ -75,6 +76,14 @@ class PolymorphicModelBase(ModelBase):
if new_class._meta.pk:
new_class.polymorphic_primary_key_name = new_class._meta.pk.name
# wrap on_delete handlers of reverse relations back to this model with the
# polymorphic deletion guard
for fk in new_class._meta.fields:
if isinstance(fk, (models.ForeignKey, models.OneToOneField)) and not isinstance(
fk.remote_field.on_delete, PolymorphicGuard
):
fk.remote_field.on_delete = PolymorphicGuard(fk.remote_field.on_delete)
return new_class
@classmethod

View File

@ -0,0 +1,64 @@
"""
Classes and utilities for handling deletions in polymorphic models.
"""
from django.db.migrations.serializer import BaseSerializer, serializer_factory
from django.db.migrations.writer import MigrationWriter
from .query import PolymorphicQuerySet
class PolymorphicGuard:
"""
Wrap an :attr:`django.db.models.ForeignKey.on_delete` callable
(CASCADE/PROTECT/SET_NULL/SET(...)/custom), but serialize as the underlying
callable.
:param action: The :attr:`django.db.models.ForeignKey.on_delete` callable to wrap.
"""
def __init__(self, action):
if not callable(action):
raise TypeError("action must be callable")
self.action = action
def __call__(self, collector, field, sub_objs, using):
"""
This guard wraps an on_delete action to ensure that any polymorphic queryset
passed to it is converted to a non-polymorphic queryset before proceeding.
This prevents issues with cascading deletes on polymorphic models.
This guard should be automatically applied to reverse relations such that
.. code-block:: python
class MyModel(PolymorphicModel):
related = models.ForeignKey(
OtherModel,
on_delete=models.CASCADE # <- equal to PolymorphicGuard(models.CASCADE)
)
"""
if isinstance(sub_objs, PolymorphicQuerySet) and not sub_objs.polymorphic_disabled:
sub_objs = sub_objs.non_polymorphic()
return self.action(collector, field, sub_objs, using)
class PolymorphicGuardSerializer(BaseSerializer):
"""
A serializer for PolymorphicGuard that serializes the underlying action.
There is no need to serialize the PolymorphicGuard itself, as it is just a wrapper
that ensures that polymorphic querysets are converted to non-polymorphic but no
polymorphic managers are present in migrations. This also ensures that new
migrations will not be generated.
"""
def serialize(self):
"""
Serialize the underlying action of the PolymorphicGuard.
"""
return serializer_factory(self.value.action).serialize()
MigrationWriter.register_serializer(PolymorphicGuard, PolymorphicGuardSerializer)

View File

@ -546,3 +546,12 @@ class PolymorphicQuerySet(QuerySet):
return olist
clist = PolymorphicQuerySet._p_list_class(olist)
return clist
def delete(self):
"""
Deletion will be done non-polymorphically because Django's multi-table deletion
mechanism is already walking the class hierarchy and producing a correct
deletion graph. Introducing polymorphic querysets into the deletion process
disrupts the model hierarchy/relationship traversal.
"""
return QuerySet.delete(self.non_polymorphic())

View File

@ -0,0 +1,871 @@
# Generated by Django 4.2 on 2025-12-20 11:18
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import polymorphic.tests.deletion.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='A_160',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='A_160Plain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='A_274',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='A_540',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
('self_referential', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='deletion.a_540')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Animal',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Answer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='B_160',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.a_160')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='B_160Plain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.a_160plain')),
],
),
migrations.CreateModel(
name='Base',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Beneficiary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstname', models.CharField(max_length=100)),
('lastname', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='CustomModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Farm',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Normal2',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Normal3',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Normal4',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Normal5',
fields=[
('n_pk', models.AutoField(primary_key=True, serialize=False)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='Title')),
],
options={
'ordering': ('title',),
},
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(blank=True, decimal_places=2, default=Decimal('0'), max_digits=10)),
('index', models.PositiveIntegerField(default=0)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.order')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'ordering': ('index',),
},
),
migrations.CreateModel(
name='PlainA',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Poll',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Poly1',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Poly2',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('normal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polies', to='deletion.normal2')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Poly4',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('normals', models.ManyToManyField(blank=True, related_name='polies', to='deletion.normal4')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Poly4_1',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='PolyDevice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
],
),
migrations.CreateModel(
name='PolyInterface',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.polydevice')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Standalone',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='A1',
fields=[
('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly1',),
),
migrations.CreateModel(
name='A2',
fields=[
('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly2',),
),
migrations.CreateModel(
name='A4',
fields=[
('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4',),
),
migrations.CreateModel(
name='A4_1',
fields=[
('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4_1',),
),
migrations.CreateModel(
name='B1',
fields=[
('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly1',),
),
migrations.CreateModel(
name='B1_160',
fields=[
('b_160_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.b_160',),
),
migrations.CreateModel(
name='B1_160Plain',
fields=[
('b_160plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160plain')),
],
bases=('deletion.b_160plain',),
),
migrations.CreateModel(
name='B2',
fields=[
('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly2',),
),
migrations.CreateModel(
name='B2_160',
fields=[
('b_160_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.b_160',),
),
migrations.CreateModel(
name='B2_160Plain',
fields=[
('b_160plain_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_160plain')),
],
bases=('deletion.b_160plain',),
),
migrations.CreateModel(
name='B4',
fields=[
('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4',),
),
migrations.CreateModel(
name='B4_1',
fields=[
('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4_1',),
),
migrations.CreateModel(
name='B_274',
fields=[
('a_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_274')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.a_274',),
),
migrations.CreateModel(
name='B_540',
fields=[
('a_540_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_540')),
('name', models.CharField(max_length=256)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.a_540',),
),
migrations.CreateModel(
name='C1',
fields=[
('poly1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly1',),
),
migrations.CreateModel(
name='C2',
fields=[
('poly2_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly2')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly2',),
),
migrations.CreateModel(
name='C4',
fields=[
('poly4_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4',),
),
migrations.CreateModel(
name='C4_1',
fields=[
('poly4_1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly4_1')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly4_1',),
),
migrations.CreateModel(
name='Cat',
fields=[
('animal_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.animal')),
('cat_param', models.CharField(max_length=100)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.animal',),
),
migrations.CreateModel(
name='Child',
fields=[
('base_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.base')),
],
bases=('deletion.base',),
),
migrations.CreateModel(
name='CreditCardPayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.payment')),
('card_type', models.CharField(max_length=32)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.payment',),
),
migrations.CreateModel(
name='D_274',
fields=[
('a_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.a_274')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.a_274',),
),
migrations.CreateModel(
name='DatasetFolder',
fields=[
('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')),
],
bases=('deletion.custommodel',),
),
migrations.CreateModel(
name='Dog',
fields=[
('animal_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.animal')),
('dog_param', models.CharField(max_length=100)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.animal',),
),
migrations.CreateModel(
name='OriginalFile',
fields=[
('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('content_type', models.CharField(max_length=100)),
('size', models.PositiveIntegerField()),
('dataset_folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.datasetfolder')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.custommodel', models.Model),
),
migrations.CreateModel(
name='PlainB1',
fields=[
('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plaina')),
('standalones', models.ManyToManyField(to='deletion.standalone')),
],
bases=('deletion.plaina',),
),
migrations.CreateModel(
name='Poly3',
fields=[
('normal3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.normal3')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.normal3', models.Model),
),
migrations.CreateModel(
name='Poly5',
fields=[
('normal5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.normal5')),
('p_pk', models.AutoField(primary_key=True, serialize=False)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.normal5', models.Model),
),
migrations.CreateModel(
name='PolyEthernetInterface',
fields=[
('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')),
('ethernety_stuff', models.CharField(max_length=64)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.polyinterface',),
),
migrations.CreateModel(
name='PolyFCInterface',
fields=[
('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')),
('fc_stuff', models.CharField(max_length=64)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.polyinterface',),
),
migrations.CreateModel(
name='PolyWirelessInterface',
fields=[
('polyinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyinterface')),
('wirelessy_stuff', models.CharField(max_length=64)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.polyinterface',),
),
migrations.CreateModel(
name='TextAnswer',
fields=[
('answer_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.answer')),
('answer', models.CharField(blank=True, default='', max_length=500)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.answer',),
),
migrations.CreateModel(
name='YesNoAnswer',
fields=[
('answer_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.answer')),
('answer', models.BooleanField(default=False, verbose_name='answer')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.answer',),
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.poll')),
],
),
migrations.AddField(
model_name='plaina',
name='standalone_parent',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainas', to='deletion.standalone'),
),
migrations.CreateModel(
name='Normal4_1',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('polies', models.ManyToManyField(blank=True, related_name='normals', to='deletion.poly4_1')),
],
),
migrations.CreateModel(
name='Normal1',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('poly', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.poly1')),
],
),
migrations.AddField(
model_name='answer',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.question'),
),
migrations.AddField(
model_name='animal',
name='farm',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='animals', to='deletion.farm'),
),
migrations.AddField(
model_name='animal',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.CreateModel(
name='A3',
fields=[
('poly3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly3')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly3',),
),
migrations.CreateModel(
name='A5',
fields=[
('poly5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.poly5')),
('a_pk', models.AutoField(primary_key=True, serialize=False)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly5',),
),
migrations.CreateModel(
name='B3',
fields=[
('poly3_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.poly3')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly3',),
),
migrations.CreateModel(
name='B5',
fields=[
('poly5_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='deletion.poly5')),
('b_pk', models.AutoField(primary_key=True, serialize=False)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.poly5',),
),
migrations.CreateModel(
name='C_274',
fields=[
('b_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.b_274')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.b_274',),
),
migrations.CreateModel(
name='DatasetRelation',
fields=[
('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')),
('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)),
('original_file_name', models.CharField(max_length=100)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.originalfile',),
),
migrations.CreateModel(
name='E_274',
fields=[
('d_274_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.d_274')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.d_274',),
),
migrations.CreateModel(
name='GrandChild',
fields=[
('child_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.child')),
],
bases=('deletion.child',),
),
migrations.CreateModel(
name='OriginalImage',
fields=[
('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')),
('original_file_name', models.CharField(max_length=100)),
('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.originalfile',),
),
migrations.CreateModel(
name='PolyFixedInterface',
fields=[
('polyethernetinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyethernetinterface')),
('fixed_stuff', models.CharField(max_length=64)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.polyethernetinterface',),
),
migrations.CreateModel(
name='PolyModularInterface',
fields=[
('polyethernetinterface_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.polyethernetinterface')),
('modular_stuff', models.CharField(max_length=64)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.polyethernetinterface',),
),
migrations.CreateModel(
name='SepaPayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.payment')),
('iban', models.CharField(max_length=34)),
('bic', models.CharField(max_length=11)),
('beneficiaries', models.ManyToManyField(blank=True, related_name='sepa', to='deletion.beneficiary')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.payment',),
),
migrations.CreateModel(
name='RelatedToChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('child', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='relatives', to='deletion.child')),
],
),
migrations.CreateModel(
name='Project',
fields=[
('custommodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.custommodel')),
('name', models.CharField(max_length=30)),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
bases=('deletion.custommodel',),
),
migrations.CreateModel(
name='PlainB2',
fields=[
('plaina_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plaina')),
('standalone', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainb2s', to='deletion.standalone')),
],
bases=('deletion.plaina',),
),
migrations.AddField(
model_name='datasetfolder',
name='prjct',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.project'),
),
migrations.CreateModel(
name='C_160Plain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.b1_160plain')),
],
),
migrations.CreateModel(
name='C_160',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='deletion.b1_160')),
],
),
migrations.CreateModel(
name='RelatedToGrandChild',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('grand_child', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='grand_relatives', to='deletion.grandchild')),
],
),
migrations.CreateModel(
name='PlainC1',
fields=[
('plainb1_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.plainb1')),
('standalone', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plainc1s', to='deletion.standalone')),
],
bases=('deletion.plainb1',),
),
migrations.CreateModel(
name='OriginalDataset',
fields=[
('originalfile_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='deletion.originalfile')),
('file', models.FileField(max_length=500, upload_to=polymorphic.tests.deletion.models.project_directory_path)),
('original_file_name', models.CharField(blank=True, max_length=100, null=True)),
('table_name', models.CharField(blank=True, max_length=100, null=True)),
('rows_number', models.PositiveIntegerField()),
('dataset_metadata', models.JSONField()),
('dataset_relation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='deletion.datasetrelation')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('deletion.originalfile',),
),
]

View File

@ -0,0 +1,491 @@
from django.db import models
from django.conf import settings
from polymorphic.models import PolymorphicModel
from polymorphic.deletion import PolymorphicGuard
from decimal import Decimal
def project_directory_path(instance, filename):
# just to satisfy upload_to; keep it deterministic for tests
return f"p{instance.dataset_folder.prjct_id}/{filename}"
CASCADE = models.CASCADE
class Standalone(models.Model):
pass
class PlainA(models.Model):
standalone_parent = models.ForeignKey(
Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainas"
)
class PlainB1(PlainA):
standalones = models.ManyToManyField(Standalone)
class PlainB2(PlainA):
standalone = models.ForeignKey(
Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainb2s"
)
class PlainC1(PlainB1):
standalone = models.ForeignKey(
Standalone, on_delete=CASCADE, null=True, default=None, related_name="plainc1s"
)
class RelatedToChild(models.Model):
child = models.ForeignKey(
"Child", on_delete=CASCADE, null=True, default=None, related_name="relatives"
)
class Base(models.Model):
pass
class Child(Base):
pass
class GrandChild(Child):
pass
class RelatedToGrandChild(models.Model):
grand_child = models.ForeignKey(
GrandChild, on_delete=CASCADE, null=True, default=None, related_name="grand_relatives"
)
###########################################################
"""
Scenario 1
<-- cascade --
Normal1 ----- FK ----> Poly1
/ | \
A1 B1 C1
"""
class Normal1(models.Model):
poly = models.ForeignKey("Poly1", on_delete=models.CASCADE) # <-- this is fine
class Poly1(PolymorphicModel):
pass
class A1(Poly1):
pass
class B1(Poly1):
pass
class C1(Poly1):
pass
"""
Scenario 2
-- cascade -->
Normal2 <----- FK ---- Poly2
/ | \
A2 B2 C2
"""
class Normal2(models.Model):
pass
class Poly2(PolymorphicModel):
normal = models.ForeignKey(Normal2, on_delete=CASCADE, related_name="polies")
class A2(Poly2):
pass
class B2(Poly2):
pass
class C2(Poly2):
pass
"""
Scenario 3
Normal3
|
Poly3
| \
A3 B3
"""
class Normal3(models.Model):
pass
class Poly3(PolymorphicModel, Normal3):
pass
class A3(Poly3):
pass
class B3(Poly3):
pass
"""
Scenario 4
<--- cascade --->
Normal4 <----- M2M -----> Poly4
/ | \
A4 B4 C4
"""
class Normal4(models.Model):
pass
class Poly4(PolymorphicModel):
normals = models.ManyToManyField(Normal4, related_name="polies", blank=True)
class A4(Poly4):
pass
class B4(Poly4):
pass
class C4(Poly4):
pass
class Normal4_1(models.Model):
polies = models.ManyToManyField("Poly4_1", related_name="normals", blank=True)
class Poly4_1(PolymorphicModel):
pass
class A4_1(Poly4_1):
pass
class B4_1(Poly4_1):
pass
class C4_1(Poly4_1):
pass
"""
Scenario 5 - scenario3 with custom/different PKs
Normal3
|
Poly3
| \
A3 B3
"""
class Normal5(models.Model):
n_pk = models.AutoField(primary_key=True)
class Poly5(PolymorphicModel, Normal5):
p_pk = models.AutoField(primary_key=True)
class A5(Poly5):
a_pk = models.AutoField(primary_key=True)
class B5(Poly5):
b_pk = models.AutoField(primary_key=True)
########################################################################################
# There were 15 years of deletion bug reports - many were duplicates but many also
# included example models. We copy all of these provided tests in here not being too
# concerned about redundancy - we just want to make sure we don't regress on any of
# them. Each block is tagged with the root issue. In some cases we mirror the setup
# with a plain django model parallel - mostly for debug/comparison purposes
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/160
class A_160(models.Model):
pass
class B_160(PolymorphicModel):
a = models.ForeignKey(A_160, on_delete=CASCADE)
class B1_160(B_160):
pass
class B2_160(B_160):
pass
class C_160(models.Model):
b = models.ForeignKey(B1_160, on_delete=CASCADE)
# Plain
class A_160Plain(models.Model):
pass
class B_160Plain(models.Model):
a = models.ForeignKey(A_160Plain, on_delete=CASCADE)
class B1_160Plain(B_160Plain):
pass
class B2_160Plain(B_160Plain):
pass
class C_160Plain(models.Model):
# test that guard misapplication is fine
b = models.ForeignKey(B1_160Plain, on_delete=PolymorphicGuard(CASCADE))
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/229
class Farm(models.Model):
pass
class Animal(PolymorphicModel):
farm = models.ForeignKey(
"Farm", on_delete=PolymorphicGuard(models.CASCADE), related_name="animals"
)
name = models.CharField(max_length=100)
class Dog(Animal):
dog_param = models.CharField(max_length=100)
class Cat(Animal):
cat_param = models.CharField(max_length=100)
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/274
class A_274(PolymorphicModel):
pass
class B_274(A_274):
pass
class D_274(A_274):
pass
class E_274(D_274):
pass
class C_274(B_274):
pass
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/357
class Order(models.Model):
title = models.CharField("Title", max_length=200)
class Meta:
ordering = ("title",)
def __str__(self):
return self.title
class Payment(PolymorphicModel):
order = models.ForeignKey(Order, on_delete=CASCADE)
amount = models.DecimalField(default=Decimal(0.0), blank=True, max_digits=10, decimal_places=2)
index = models.PositiveIntegerField(default=0, blank=False)
class Meta:
ordering = ("index",)
class CreditCardPayment(Payment):
card_type = models.CharField(max_length=32)
class Beneficiary(models.Model):
firstname = models.CharField(max_length=100)
lastname = models.CharField(max_length=100)
class SepaPayment(Payment):
iban = models.CharField(max_length=34)
bic = models.CharField(max_length=11)
beneficiaries = models.ManyToManyField(Beneficiary, "sepa", blank=True)
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/481
# no example given - how to?
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/540
class A_540(PolymorphicModel):
self_referential = models.ForeignKey("self", null=True, blank=True, on_delete=CASCADE)
class B_540(A_540):
name = models.CharField(max_length=256)
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/547
class CustomModel(models.Model):
pass
class Project(CustomModel):
name = models.CharField(max_length=30)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class DatasetFolder(CustomModel):
prjct = models.ForeignKey(Project, on_delete=CASCADE)
class OriginalFile(PolymorphicModel, CustomModel):
dataset_folder = models.ForeignKey(DatasetFolder, on_delete=CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
content_type = models.CharField(max_length=100)
size = models.PositiveIntegerField()
class DatasetRelation(OriginalFile):
file = models.FileField(max_length=500, upload_to=project_directory_path)
original_file_name = models.CharField(max_length=100)
class OriginalDataset(OriginalFile):
dataset_relation = models.ForeignKey(
DatasetRelation, on_delete=models.SET_NULL, blank=True, null=True
)
file = models.FileField(max_length=500, upload_to=project_directory_path)
original_file_name = models.CharField(max_length=100, null=True, blank=True)
table_name = models.CharField(max_length=100, null=True, blank=True)
rows_number = models.PositiveIntegerField()
dataset_metadata = models.JSONField()
class OriginalImage(OriginalFile):
original_file_name = models.CharField(max_length=100)
file = models.FileField(max_length=500, upload_to=project_directory_path)
###########################################################
###########################################################
# https://github.com/jazzband/django-polymorphic/issues/608
class PolyDevice(models.Model):
name = models.CharField(max_length=64)
class PolyInterface(PolymorphicModel):
name = models.CharField(max_length=64)
device = models.ForeignKey(to=PolyDevice, on_delete=CASCADE)
class PolyEthernetInterface(PolyInterface):
ethernety_stuff = models.CharField(max_length=64)
class PolyModularInterface(PolyEthernetInterface):
modular_stuff = models.CharField(max_length=64)
class PolyFixedInterface(PolyEthernetInterface):
fixed_stuff = models.CharField(max_length=64)
class PolyWirelessInterface(PolyInterface):
wirelessy_stuff = models.CharField(max_length=64)
class PolyFCInterface(PolyInterface):
fc_stuff = models.CharField(max_length=64)
class Poll(models.Model):
pass
class Question(models.Model):
poll = models.ForeignKey(to=Poll, on_delete=CASCADE)
class Answer(PolymorphicModel):
question = models.ForeignKey(to=Question, on_delete=CASCADE)
class TextAnswer(Answer):
answer = models.CharField(default="", blank=True, max_length=500)
class YesNoAnswer(Answer):
answer = models.BooleanField("answer", default=False)

View File

@ -0,0 +1,741 @@
from django.test import TestCase
import shutil
import tempfile
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.test.utils import CaptureQueriesContext
from django.test import override_settings
from django.db import connection
@override_settings()
class TestDeletion(TestCase):
def setUp(self):
super().setUp()
self._media_root = tempfile.mkdtemp(prefix="test-media-")
self._media_override = override_settings(MEDIA_ROOT=self._media_root)
self._media_override.enable()
def tearDown(self):
self._media_override.disable()
shutil.rmtree(self._media_root, ignore_errors=True)
super().tearDown()
def test_deletion_bug_160(self):
"""https://github.com/jazzband/django-polymorphic/issues/160"""
from .models import A_160, B_160, B1_160, B2_160, C_160
from .models import A_160Plain, B_160Plain, C_160Plain, B1_160Plain, B2_160Plain
a = A_160Plain.objects.create()
a2 = A_160Plain.objects.create()
b1 = B1_160Plain.objects.create(a=a)
b2 = B2_160Plain.objects.create(a=a)
b2_2 = B2_160Plain.objects.create(a=a2)
c = C_160Plain.objects.create(b=b1)
a.delete()
assert [a2] == list(A_160Plain.objects.all())
assert B_160Plain.objects.count() == 1
assert B1_160Plain.objects.count() == 0
assert [b2_2] == list(B2_160Plain.objects.all())
assert C_160Plain.objects.count() == 0
a = A_160.objects.create()
a2 = A_160.objects.create()
b1 = B1_160.objects.create(a=a)
b2 = B2_160.objects.create(a=a)
b2_2 = B2_160.objects.create(a=a2)
c = C_160.objects.create(b=b1)
a.delete()
assert [a2] == list(A_160.objects.all())
assert B_160.objects.count() == 1
assert B1_160.objects.count() == 0
assert [b2_2] == list(B2_160.objects.all())
assert C_160.objects.count() == 0
def test_deletion_bug_229(self):
"""
https://github.com/jazzband/django-polymorphic/issues/229
"""
from .models import Farm, Animal, Dog, Cat
farm = Farm.objects.create()
Dog.objects.create(farm=farm, name="Rex", dog_param="kibble")
Cat.objects.create(farm=farm, name="Misty", cat_param="whiskers")
farm.delete()
assert Animal.objects.count() == 0
assert Dog.objects.count() == 0
assert Cat.objects.count() == 0
assert Farm.objects.count() == 0
farm = Farm.objects.create()
Dog.objects.create(farm=farm, name="Rex", dog_param="kibble")
Cat.objects.create(farm=farm, name="Misty", cat_param="whiskers")
farm2 = Farm.objects.create()
hugo = Cat.objects.create(farm=farm2, name="Hugo", cat_param="10")
marlo = Cat.objects.create(farm=farm2, name="Marlo", cat_param="14")
assert Animal.objects.count() == 4
farm.delete()
assert Animal.objects.count() == 2
assert hugo in Cat.objects.all()
assert marlo in Cat.objects.all()
assert hugo in Animal.objects.all()
assert marlo in Animal.objects.all()
assert hugo in farm2.animals.all()
assert marlo in farm2.animals.all()
Animal.objects.all().delete()
assert Animal.objects.count() == 0
assert Dog.objects.count() == 0
assert Cat.objects.count() == 0
assert Farm.objects.count() == 1
assert farm2 in Farm.objects.all()
assert farm2.animals.count() == 0
def test_deletion_bug_274(self):
"""
https://github.com/jazzband/django-polymorphic/issues/274
"""
from .models import A_274, B_274, C_274, D_274, E_274
B_274.objects.create()
D_274.objects.create()
E_274.objects.create()
A_274.objects.all().delete()
assert A_274.objects.count() == 0
assert B_274.objects.count() == 0
assert D_274.objects.count() == 0
assert E_274.objects.count() == 0
assert C_274.objects.count() == 0
def test_deletion_bug_357(self):
"""
https://github.com/jazzband/django-polymorphic/issues/357
"""
from .models import Order, Payment, CreditCardPayment, SepaPayment, Beneficiary
order1 = Order.objects.create(title="Order 1")
payment1 = SepaPayment.objects.create(
order=order1,
amount=100.00,
iban="DE89370400440532013000",
bic="COBADEFFXXX",
)
CreditCardPayment.objects.create(
order=order1,
amount=100.00,
card_type="VISA",
)
bk = Beneficiary.objects.create(firstname="Brian", lastname="Kohan")
ea = Beneficiary.objects.create(firstname="Edward", lastname="Abbey")
payment1.beneficiaries.add(bk, ea)
Order.objects.all().delete()
assert Order.objects.count() == 0
assert Payment.objects.count() == 0
assert set(Beneficiary.objects.all()) == {bk, ea}
def test_deletion_bug_540(self):
"""
https://github.com/jazzband/django-polymorphic/issues/540
"""
from .models import A_540, B_540
b = B_540.objects.create(self_referential=None, name="b")
a = A_540.objects.create(self_referential=b)
A_540.objects.all().delete()
assert A_540.objects.count() == 0
assert B_540.objects.count() == 0
def test_deletion_bug_547(self):
"""
https://github.com/jazzband/django-polymorphic/issues/547
"""
from .models import Project, DatasetFolder, OriginalFile, DatasetRelation, OriginalDataset
User = get_user_model()
user = User.objects.create_user(username="u1", password="x")
project = Project.objects.create(name="p1", created_by=user)
folder = DatasetFolder.objects.create(prjct=project)
rel_bytes = b"id,parent_id\n1,\n2,1\n"
rel_file = ContentFile(rel_bytes, name="relations.csv")
relation = DatasetRelation.objects.create(
dataset_folder=folder,
content_type="text/csv",
size=len(rel_bytes),
file=rel_file,
original_file_name="relations.csv",
)
ds_bytes = b"id,value\n1,foo\n2,bar\n"
ds_file = ContentFile(ds_bytes, name="data.csv")
OriginalDataset.objects.create(
dataset_folder=folder,
content_type="text/csv",
size=len(ds_bytes),
dataset_relation=relation,
file=ds_file,
original_file_name="data.csv",
table_name="data",
rows_number=2,
dataset_metadata={"columns": ["id", "value"]},
)
# This is the operation that (per report) can crash with:
# AttributeError: 'NoneType' object has no attribute 'attname'
#
# If the bug is present, this test will error here.
project.delete()
# If deletion succeeded, everything should be gone.
assert Project.objects.count() == 0
assert DatasetFolder.objects.count() == 0
assert OriginalFile.objects.count() == 0
assert DatasetRelation.objects.count() == 0
assert OriginalDataset.objects.count() == 0
def test_deletion_bug_608(self):
"""
https://github.com/jazzband/django-polymorphic/issues/608
"""
from .models import (
PolyDevice,
PolyEthernetInterface,
PolyFCInterface,
PolyFixedInterface,
PolyInterface,
PolyModularInterface,
PolyWirelessInterface,
# NotPolyInterface
)
device = PolyDevice.objects.create(name="Device 1")
PolyEthernetInterface.objects.create(name="Eth0", device=device, ethernety_stuff="stuff")
PolyFCInterface.objects.create(name="FC0", device=device, fc_stuff="stuff")
PolyFixedInterface.objects.create(name="Fixed0", device=device, fixed_stuff="stuff")
PolyModularInterface.objects.create(name="Modular0", device=device, modular_stuff="stuff")
PolyWirelessInterface.objects.create(
name="Wireless0", device=device, wirelessy_stuff="stuff"
)
PolyDevice.objects.all().delete()
assert PolyDevice.objects.count() == 0
def test_deletion_bug_608_2(self):
"""
https://github.com/jazzband/django-polymorphic/issues/608
"""
from .models import Poll, Question, Answer, TextAnswer, YesNoAnswer
poll = Poll.objects.create()
question = Question.objects.create(poll=poll)
answer1 = TextAnswer.objects.create(question=question, answer="test")
answer2 = YesNoAnswer.objects.create(question=question, answer=True)
poll.delete()
assert Poll.objects.count() == 0
assert Question.objects.count() == 0
assert Answer.objects.count() == 0
assert TextAnswer.objects.count() == 0
assert YesNoAnswer.objects.count() == 0
def test_vanilla_deletion(self):
"""
Test Django's vanilla multi table inheritance deletion and signaling.
PlainA *-----> Standalone
/ \
Standalone *------* PlainB1 PlainB2 *------> Standalone
|
PlainC1 *------> Standalone
"""
from .models import (
PlainA,
PlainB1,
PlainC1,
PlainB2,
Standalone,
RelatedToChild,
Base,
Child,
GrandChild,
RelatedToGrandChild,
)
print("---------------------------")
PlainA.objects.create()
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina"
SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (1)
DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (1)
DELETE FROM "plaina" WHERE "plaina"."id" IN (1)
"""
PlainA.objects.all().delete()
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
PlainB1.objects.create()
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id")
SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."plainb1_ptr_id" IN (2)
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (2)
DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (2)
DELETE FROM "plaina" WHERE "plaina"."id" IN (2)
"""
PlainB1.objects.all().delete()
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
PlainC1.objects.create()
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id", "plainc1"."standalone_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id")
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (3)
DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (3)
DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (3)
DELETE FROM "plaina" WHERE "plaina"."id" IN (3)
"""
PlainC1.objects.all().delete()
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
PlainC1.objects.create()
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina"
SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (4)
SELECT "plaina"."id", "plaina"."standalone_parent_id" FROM "plaina" WHERE "plaina"."id" = 4 LIMIT 21
SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."plainb1_ptr_id" IN (4)
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") WHERE "plainb1"."plaina_ptr_id" = 4 LIMIT 21
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (4)
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (4)
DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (4)
DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (4)
DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (4)
DELETE FROM "plaina" WHERE "plaina"."id" IN (4)
"""
PlainA.objects.all().delete()
assert PlainC1.objects.count() == 0
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
s0 = Standalone.objects.create()
PlainC1.objects.create(standalone=s0)
PlainB2.objects.create(standalone=s0)
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "standalone"."id" FROM "standalone"
SELECT "plaina"."id" FROM "plaina" WHERE "plaina"."standalone_parent_id" IN (1)
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb2"."plaina_ptr_id", "plainb2"."standalone_id" FROM "plainb2" INNER JOIN "plaina" ON ("plainb2"."plaina_ptr_id" = "plaina"."id") WHERE "plainb2"."standalone_id" IN (1)
SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."standalone_id" IN (1)
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb1"."plaina_ptr_id" FROM "plainb1" INNER JOIN "plaina" ON ("plainb1"."plaina_ptr_id" = "plaina"."id") WHERE "plainb1"."plaina_ptr_id" = 5 LIMIT 21
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."plainb1_id" IN (5)
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."standalone_id" IN (1)
DELETE FROM "standalone" WHERE "standalone"."id" IN (1)
DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (6)
DELETE FROM "plainc1" WHERE "plainc1"."plainb1_ptr_id" IN (5)
DELETE FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (5)
DELETE FROM "plaina" WHERE "plaina"."id" IN (6, 5)
"""
Standalone.objects.all().delete()
assert PlainC1.objects.count() == 0
assert PlainB2.objects.count() == 0
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
s0 = Standalone.objects.create()
s1 = Standalone.objects.create()
PlainB2.objects.create(standalone_parent=s0, standalone=s1)
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "plaina"."id" FROM "plaina" WHERE "plaina"."standalone_parent_id" IN (2)
SELECT "plainb1"."plaina_ptr_id" FROM "plainb1" WHERE "plainb1"."plaina_ptr_id" IN (7)
SELECT "plaina"."id", "plaina"."standalone_parent_id", "plainb2"."plaina_ptr_id", "plainb2"."standalone_id" FROM "plainb2" INNER JOIN "plaina" ON ("plainb2"."plaina_ptr_id" = "plaina"."id") WHERE "plainb2"."standalone_id" IN (2)
SELECT "plainb1"."plaina_ptr_id", "plainc1"."plainb1_ptr_id" FROM "plainc1" INNER JOIN "plainb1" ON ("plainc1"."plainb1_ptr_id" = "plainb1"."plaina_ptr_id") WHERE "plainc1"."standalone_id" IN (2)
DELETE FROM "plainb2" WHERE "plainb2"."plaina_ptr_id" IN (7)
DELETE FROM "plainb1_standalones" WHERE "plainb1_standalones"."standalone_id" IN (2)
DELETE FROM "standalone" WHERE "standalone"."id" IN (2)
DELETE FROM "plaina" WHERE "plaina"."id" IN (7)
"""
s0.delete()
assert PlainB2.objects.count() == 0
assert list(Standalone.objects.all()) == [s1]
# for q in ctx.captured_queries:
# print(q["sql"])
print("---------------------------")
grand_child = GrandChild.objects.create()
RelatedToChild.objects.create(child=Child.objects.get(pk=grand_child.pk))
RelatedToGrandChild.objects.create(grand_child=grand_child)
with CaptureQueriesContext(connection) as ctx:
"""
SELECT "base"."id" FROM "base" WHERE "base"."id" = 1
SELECT "child"."base_ptr_id" FROM "child" WHERE "child"."base_ptr_id" IN (1)
SELECT "base"."id" FROM "base" WHERE "base"."id" = 1 LIMIT 21
SELECT "child"."base_ptr_id", "grandchild"."child_ptr_id" FROM "grandchild" INNER JOIN "child" ON ("grandchild"."child_ptr_id" = "child"."base_ptr_id") WHERE "grandchild"."child_ptr_id" IN (1)
SELECT "base"."id", "child"."base_ptr_id" FROM "child" INNER JOIN "base" ON ("child"."base_ptr_id" = "base"."id") WHERE "child"."base_ptr_id" = 1 LIMIT 21
DELETE FROM "relatedtochild" WHERE "relatedtochild"."child_id" IN (1)
DELETE FROM "relatedtograndchild" WHERE "relatedtograndchild"."grand_child_id" IN (1)
DELETE FROM "relatedtochild" WHERE "relatedtochild"."child_id" IN (1)
DELETE FROM "grandchild" WHERE "grandchild"."child_ptr_id" IN (1)
DELETE FROM "child" WHERE "child"."base_ptr_id" IN (1)
DELETE FROM "base" WHERE "base"."id" IN (1)
"""
Base.objects.filter(pk=grand_child.pk).delete() # cascade should reach relatives!
assert Base.objects.count() == 0
assert Child.objects.count() == 0
assert GrandChild.objects.count() == 0
assert RelatedToChild.objects.count() == 0
assert RelatedToGrandChild.objects.count() == 0
# for q in ctx.captured_queries:
# print(q["sql"])
def test_polymorphic_deletion_scenario1(self):
"""
Test the first polymorphic deletion scenario:
A normal model holds a foreign key to a polymorphic base model with several
children.
<-- cascade --
Normal1 ----- FK ----> Poly1
/ | \
A1 B1 C1
Tests that when you delete from a poly instance at any level of the
poly hierarchy, cascading deletion propagates correctly to Normal1.
And deleting Normal1 also works with no effects on the poly instances.
"""
from .models import (
Normal1,
Poly1,
A1,
B1,
C1,
)
p1 = Poly1.objects.create()
a1 = A1.objects.create()
b1 = B1.objects.create()
c1 = C1.objects.create()
n1 = Normal1.objects.create(poly=p1)
n2 = Normal1.objects.create(poly=c1)
n3 = Normal1.objects.create(poly=a1)
n4 = Normal1.objects.create(poly=b1)
n5 = Normal1.objects.create(poly=p1)
n6 = Normal1.objects.create(poly=b1)
n7 = Normal1.objects.create(poly=b1)
n8 = Normal1.objects.create(poly=c1)
# test delete from parent
Poly1.objects.filter(pk=a1.pk).delete()
assert Normal1.objects.count() == 7
assert n3 not in Normal1.objects.all()
assert A1.objects.count() == 0
Poly1.objects.filter(pk=p1.pk).delete()
assert Normal1.objects.count() == 5
assert n1 not in Normal1.objects.all()
assert n5 not in Normal1.objects.all()
n6.delete()
assert Normal1.objects.count() == 4
assert B1.objects.count() == 1
assert C1.objects.count() == 1
assert b1 in Poly1.objects.all()
assert c1 in Poly1.objects.all()
Poly1.objects.all().delete()
assert Normal1.objects.count() == 0
assert Poly1.objects.count() == 0
def test_polymorphic_deletion_scenario2(self):
"""
Test the second polymorphic deletion scenario:
A polymorphic model holds a foreign key to a base model with several
children.
-- cascade -->
Normal2 <----- FK ---- Poly2
/ | \
A2 B2 C2
Tests that when you delete a normal instance all related poly instances cascade
correctly regardless of their concrete type in the hierarchy.
This is the major collector failure mode.
"""
from .models import (
Normal2,
Poly2,
A2,
B2,
C2,
)
n1, n2, n3, n4 = (
Normal2.objects.create(),
Normal2.objects.create(),
Normal2.objects.create(),
Normal2.objects.create(),
)
p1, p2 = Poly2.objects.create(normal=n1), Poly2.objects.create(normal=n4)
a1, a2 = A2.objects.create(normal=n2), A2.objects.create(normal=n3)
b1, b2 = B2.objects.create(normal=n4), B2.objects.create(normal=n2)
c1, c2 = C2.objects.create(normal=n3), C2.objects.create(normal=n1)
assert set(n1.polies.all()) == {p1, c2}
assert set(n2.polies.all()) == {a1, b2}
assert set(n3.polies.all()) == {a2, c1}
assert set(n4.polies.all()) == {p2, b1}
n2.delete()
assert Poly2.objects.count() == 6
assert a1 not in Poly2.objects.all()
assert b2 not in Poly2.objects.all()
Normal2.objects.filter(pk__in=[n1.pk, n4.pk]).delete()
assert Poly2.objects.count() == 2
assert p1 not in Poly2.objects.all()
assert c2 not in Poly2.objects.all()
assert p2 not in Poly2.objects.all()
assert b1 not in Poly2.objects.all()
n3.delete()
assert Poly2.objects.count() == 0
def test_polymorphic_deletion_scenario3(self):
"""
Scenario 3
Normal3
|
Poly3
| \
A3 B3
Deleting Poly3 should cascade delete Normal3 and deleting from Normal3 should
cascade down to children.
"""
from .models import (
Normal3,
Poly3,
A3,
B3,
)
b1 = B3.objects.create()
assert b1 in Poly3.objects.all()
Normal3.objects.filter(pk=b1.pk).delete()
assert b1 not in Poly3.objects.all()
assert Poly3.objects.count() == 0
b2 = B3.objects.create()
assert Normal3.objects.filter(pk=b2.pk).exists()
assert b2 in Poly3.objects.all()
b2.delete()
assert not Normal3.objects.filter(pk=b2.pk).exists()
assert Poly3.objects.count() == 0
def test_polymorphic_deletion_scenario4(self):
"""
Scenario 4 - M2Ms between normal/poly models
<--- cascade --->
Normal4 <----- M2M -----> Poly4
/ | \
A4 B4 C4
Ensure relations are appropriately cascaded on deletions from either side.
"""
from .models import (
Normal4,
Poly4,
A4,
B4,
C4,
)
n1, n2, n3, n4 = (
Normal4.objects.create(),
Normal4.objects.create(),
Normal4.objects.create(),
Normal4.objects.create(),
)
p1, p2 = Poly4.objects.create(), Poly4.objects.create()
a1, a2 = A4.objects.create(), A4.objects.create()
b1, b2 = B4.objects.create(), B4.objects.create()
c1, c2 = C4.objects.create(), C4.objects.create()
n1.polies.add(p1, a1, b1, c1)
n2.polies.add(p2, a2, b2, c2)
n3.polies.add(b1, c1)
n4.polies.add(p2, c2)
assert set(n1.polies.all()) == {p1, a1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(n4.polies.all()) == {p2, c2}
a1.delete()
assert set(n1.polies.all()) == {p1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(n4.polies.all()) == {p2, c2}
n4.delete()
assert set(n1.polies.all()) == {p1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(p2.normals.all()) == {n2}
assert set(c2.normals.all()) == {n2}
Poly4.objects.all().delete()
assert n1.polies.count() == 0
assert n2.polies.count() == 0
assert n3.polies.count() == 0
def test_polymorphic_deletion_scenario4_1(self):
"""
Scenario 4 - M2Ms between normal/poly models
<--- cascade --->
Normal4_1 <----- M2M -----> Poly4
/ | \
A4 B4 C4
Ensure relations are appropriately cascaded on deletions from either side.
"""
from .models import (
Normal4_1,
Poly4_1,
A4_1,
B4_1,
C4_1,
)
n1, n2, n3, n4 = (
Normal4_1.objects.create(),
Normal4_1.objects.create(),
Normal4_1.objects.create(),
Normal4_1.objects.create(),
)
p1, p2 = Poly4_1.objects.create(), Poly4_1.objects.create()
a1, a2 = A4_1.objects.create(), A4_1.objects.create()
b1, b2 = B4_1.objects.create(), B4_1.objects.create()
c1, c2 = C4_1.objects.create(), C4_1.objects.create()
n1.polies.add(p1, a1, b1, c1)
n2.polies.add(p2, a2, b2, c2)
n3.polies.add(b1, c1)
n4.polies.add(p2, c2)
assert set(n1.polies.all()) == {p1, a1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(n4.polies.all()) == {p2, c2}
a1.delete()
assert set(n1.polies.all()) == {p1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(n4.polies.all()) == {p2, c2}
n4.delete()
assert set(n1.polies.all()) == {p1, b1, c1}
assert set(n2.polies.all()) == {p2, a2, b2, c2}
assert set(n3.polies.all()) == {b1, c1}
assert set(p2.normals.all()) == {n2}
assert set(c2.normals.all()) == {n2}
Poly4_1.objects.all().delete()
assert n1.polies.count() == 0
assert n2.polies.count() == 0
assert n3.polies.count() == 0
def test_polymorphic_deletion_scenario5(self):
"""
Scenario 5 - scenario3 with custom/different PKs
Normal5
|
Poly5
| \
A5 B5
Deleting Poly5 should cascade delete Normal5 and deleting from Normal5 should
cascade down to children.
"""
from .models import (
Normal5,
Poly5,
A5,
B5,
)
b1 = B5.objects.create()
assert b1 in Poly5.objects.all()
Normal5.objects.filter(pk=b1.pk).delete()
assert b1 not in Poly5.objects.all()
assert Poly5.objects.count() == 0
b2 = B5.objects.create()
assert Normal5.objects.filter(pk=b2.pk).exists()
assert b2 in Poly5.objects.all()
b2.delete()
assert not Normal5.objects.filter(pk=b2.pk).exists()
assert Poly5.objects.count() == 0
# FIXME: django-polymorphic assumes all rows share the same PK value
# n = Normal5.objects.create(n_pk=100)
# p = Poly5.objects.create_from_super(n, p_pk=200)
# A5.objects.create_from_super(p, a_pk=300)
# b = B5.objects.create_from_super(p, b_pk=400)
# assert Poly5.objects.count() == 1
# assert b in Poly5.objects.all()
# Normal5.objects.filter(pk=n.pk).delete()
# assert Poly5.objects.count() == 0
# n1 = Normal5.objects.create(n_pk=101)
# p1 = Poly5.objects.create_from_super(n1, p_pk=201)
# A5.objects.create_from_super(p1, a_pk=301)
# b1 = B5.objects.create_from_super(p1, b_pk=401)
# assert Poly5.objects.count() == 1
# assert b1 in Poly5.objects.all()
# b1.delete()
# assert Poly5.objects.count() == 0
# assert Normal5.objects.count() == 0

View File

@ -32,3 +32,20 @@ class TestErrata(SimpleTestCase):
assert any("not_instance_of" in msg for msg in error_messages), (
f"Expected error for 'not_instance_of' field but got: {error_messages}"
)
def test_polymorphic_guard_requires_callable(self):
"""Test that PolymorphicGuard raises TypeError if initialized with non-callable."""
from polymorphic.deletion import PolymorphicGuard
non_callable_values = [42, "not a function", None, 3.14, [], {}]
for value in non_callable_values:
try:
PolymorphicGuard(value)
except TypeError as e:
assert str(e) == "action must be callable", (
f"Expected TypeError with message 'action must be callable' but got: {e}"
)
else:
assert False, f"Expected TypeError when initializing PolymorphicGuard with {value}"

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2025-12-16 21:55
# Generated by Django 4.2 on 2025-12-20 11:18
from django.conf import settings
from django.db import migrations, models

View File

@ -100,6 +100,8 @@ INSTALLED_APPS = (
"django.contrib.admin",
"polymorphic",
"polymorphic.tests",
"polymorphic.tests.deletion",
"polymorphic.tests.test_migrations",
)
MIDDLEWARE = (

View File

@ -0,0 +1,107 @@
from django.db import models
from polymorphic.models import PolymorphicModel
def get_default_related():
"""Default function for SET() callable"""
return None
class RelatedModel(models.Model):
"""A regular non-polymorphic model that will be referenced"""
name = models.CharField(max_length=100)
class BasePolyModel(PolymorphicModel):
"""
Base polymorphic model to test that PolymorphicGuard wraps
on_delete handlers properly and serializes them correctly.
"""
name = models.CharField(max_length=100)
class ChildPolyModel(BasePolyModel):
"""Child polymorphic model"""
description = models.CharField(max_length=200, blank=True)
class GrandChildPolyModel(ChildPolyModel):
"""Grandchild polymorphic model"""
extra_info = models.CharField(max_length=200, blank=True)
# Models with ForeignKey using different on_delete behaviors
# These should all be wrapped with PolymorphicGuard automatically
class ModelWithCascade(PolymorphicModel):
"""Test CASCADE on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.CASCADE)
class ModelWithProtect(PolymorphicModel):
"""Test PROTECT on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.PROTECT)
class ModelWithSetNull(PolymorphicModel):
"""Test SET_NULL on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.SET_NULL, null=True)
class ModelWithSetDefault(PolymorphicModel):
"""Test SET_DEFAULT on_delete"""
related = models.ForeignKey(
RelatedModel, on_delete=models.SET_DEFAULT, null=True, default=None
)
class ModelWithSet(PolymorphicModel):
"""Test SET(...) on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.SET(get_default_related), null=True)
class ModelWithDoNothing(PolymorphicModel):
"""Test DO_NOTHING on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.DO_NOTHING)
class ModelWithRestrict(PolymorphicModel):
"""Test RESTRICT on_delete"""
related = models.ForeignKey(RelatedModel, on_delete=models.RESTRICT)
# OneToOneField tests
class ModelWithOneToOneCascade(PolymorphicModel):
"""Test CASCADE on_delete with OneToOneField"""
related = models.OneToOneField(RelatedModel, on_delete=models.CASCADE)
class ModelWithOneToOneProtect(PolymorphicModel):
"""Test PROTECT on_delete with OneToOneField"""
related = models.OneToOneField(
RelatedModel, on_delete=models.PROTECT, related_name="one_to_one_protect"
)
class ModelWithOneToOneSetNull(PolymorphicModel):
"""Test SET_NULL on_delete with OneToOneField"""
related = models.OneToOneField(
RelatedModel, on_delete=models.SET_NULL, null=True, related_name="one_to_one_set_null"
)

View File

@ -0,0 +1,584 @@
"""
Tests for PolymorphicGuard serialization of on_delete functions.
This test module ensures that all Django on_delete handlers (CASCADE, PROTECT,
SET_NULL, SET_DEFAULT, SET(...), DO_NOTHING, and RESTRICT) are properly wrapped
with PolymorphicGuard and serialize correctly in migrations.
"""
import shutil
from pathlib import Path
from django.test import TestCase, TransactionTestCase
from django.db import models
from django.db.migrations.serializer import serializer_factory
from django.db.models import ProtectedError, RestrictedError
from ..utils import GeneratedMigrationsPerClassMixin
from polymorphic.deletion import PolymorphicGuard
from polymorphic.managers import PolymorphicManager
from polymorphic.query import PolymorphicQuerySet
class OnDeleteSerializationTest(GeneratedMigrationsPerClassMixin, TransactionTestCase):
"""
Test that PolymorphicGuard wraps on_delete handlers and serializes them correctly.
"""
apps_to_migrate: list[str] = ["test_migrations"]
@property
def state(self):
return self._applied_states["test_migrations"]
@classmethod
def setUpClass(cls):
"""Set up by generating and applying migrations for test_migrations app"""
super().setUpClass()
cls.migrations_dir = Path(__file__).parent / "migrations"
def test_migration_managers_non_polymorphic(self):
for mdl in [
"BasePolyModel",
"ChildPolyModel",
"GrandChildPolyModel",
"ModelWithCascade",
"ModelWithProtect",
"ModelWithSetNull",
"ModelWithSetDefault",
"ModelWithSet",
"ModelWithDoNothing",
"ModelWithRestrict",
"ModelWithOneToOneCascade",
"ModelWithOneToOneProtect",
"ModelWithOneToOneSetNull",
]:
Model = self.state.apps.get_model("test_migrations", mdl)
managers = Model._meta.managers
assert not isinstance(Model.objects, (PolymorphicManager, PolymorphicQuerySet))
assert all(not isinstance(m, (PolymorphicManager, PolymorphicQuerySet)) for m in managers)
RelatedModel = self.state.apps.get_model("test_migrations", "RelatedModel")
related = RelatedModel.objects.create(name="tester")
ModelWithOneToOneCascade = self.state.apps.get_model(
"test_migrations", "ModelWithOneToOneCascade"
)
ModelWithOneToOneProtect = self.state.apps.get_model(
"test_migrations", "ModelWithOneToOneProtect"
)
ModelWithOneToOneSetNull = self.state.apps.get_model(
"test_migrations", "ModelWithOneToOneSetNull"
)
ModelWithOneToOneCascade.objects.create(related=related)
ModelWithOneToOneProtect.objects.create(related=related)
ModelWithOneToOneSetNull.objects.create(related=related)
for relation in [
"modelwithcascade_set",
"modelwithprotect_set",
"modelwithsetnull_set",
"modelwithsetdefault_set",
"modelwithset_set",
"modelwithdonothing_set",
"modelwithrestrict_set",
"modelwithonetoonecascade",
"one_to_one_protect",
"one_to_one_set_null",
]:
assert not isinstance(
getattr(related, relation), (PolymorphicManager, PolymorphicQuerySet)
)
def test_foreign_keys_wrapped_with_PolymorphicGuard(self):
"""Verify that ForeignKey on_delete handlers are wrapped with PolymorphicGuard"""
from .models import (
ModelWithCascade,
ModelWithProtect,
ModelWithSetNull,
ModelWithSetDefault,
ModelWithSet,
ModelWithDoNothing,
ModelWithRestrict,
)
# Get the 'related' field from each model
models_to_test = [
ModelWithCascade,
ModelWithProtect,
ModelWithSetNull,
ModelWithSetDefault,
ModelWithSet,
ModelWithDoNothing,
ModelWithRestrict,
]
for model_class in models_to_test:
with self.subTest(model=model_class.__name__):
field = model_class._meta.get_field("related")
on_delete = field.remote_field.on_delete
# Assert that the on_delete handler is wrapped with PolymorphicGuard
self.assertIsInstance(
on_delete,
PolymorphicGuard,
f"{model_class.__name__}.related field should have PolymorphicGuard wrapper",
)
def test_one_to_one_wrapped_with_PolymorphicGuard(self):
"""Verify that OneToOneField on_delete handlers are wrapped with PolymorphicGuard"""
from .models import (
ModelWithOneToOneCascade,
ModelWithOneToOneProtect,
ModelWithOneToOneSetNull,
)
models_to_test = [
ModelWithOneToOneCascade,
ModelWithOneToOneProtect,
ModelWithOneToOneSetNull,
]
for model_class in models_to_test:
with self.subTest(model=model_class.__name__):
field = model_class._meta.get_field("related")
on_delete = field.remote_field.on_delete
# Assert that the on_delete handler is wrapped with PolymorphicGuard
self.assertIsInstance(
on_delete,
PolymorphicGuard,
f"{model_class.__name__}.related field should have PolymorphicGuard wrapper",
)
def test_cascade_serialization(self):
"""Test that CASCADE serializes correctly through PolymorphicGuard"""
from .models import ModelWithCascade
field = ModelWithCascade._meta.get_field("related")
on_delete = field.remote_field.on_delete
# Serialize the PolymorphicGuard wrapped CASCADE
serialized, imports = serializer_factory(on_delete).serialize()
# Should serialize as CASCADE from django.db.models.deletion, not as PolymorphicGuard
self.assertIn("CASCADE", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_protect_serialization(self):
"""Test that PROTECT serializes correctly through PolymorphicGuard"""
from .models import ModelWithProtect
field = ModelWithProtect._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
self.assertIn("PROTECT", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_set_null_serialization(self):
"""Test that SET_NULL serializes correctly through PolymorphicGuard"""
from .models import ModelWithSetNull
field = ModelWithSetNull._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
self.assertIn("SET_NULL", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_set_default_serialization(self):
"""Test that SET_DEFAULT serializes correctly through PolymorphicGuard"""
from .models import ModelWithSetDefault
field = ModelWithSetDefault._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
self.assertIn("SET_DEFAULT", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_set_callable_serialization(self):
"""Test that SET(...) with a callable serializes correctly through PolymorphicGuard"""
from .models import ModelWithSet
field = ModelWithSet._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
# Should serialize the SET() function with the callable reference
self.assertIn("SET", serialized)
self.assertIn("get_default_related", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_do_nothing_serialization(self):
"""Test that DO_NOTHING serializes correctly through PolymorphicGuard"""
from .models import ModelWithDoNothing
field = ModelWithDoNothing._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
self.assertIn("DO_NOTHING", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_restrict_serialization(self):
"""Test that RESTRICT serializes correctly through PolymorphicGuard"""
from .models import ModelWithRestrict
field = ModelWithRestrict._meta.get_field("related")
on_delete = field.remote_field.on_delete
serialized, imports = serializer_factory(on_delete).serialize()
self.assertIn("RESTRICT", serialized)
self.assertNotIn("PolymorphicGuard", serialized)
def test_migration_file_generated(self):
"""Test that a migration file was generated"""
# Check that at least one migration file was created
migration_files = list(self.migrations_dir.glob("0001_*.py"))
self.assertTrue(len(migration_files) > 0, "No migration file was generated")
def test_migration_file_content(self):
"""Test that the generated migration file contains correct serialization"""
# Find the initial migration file
migration_files = list(self.migrations_dir.glob("0001_*.py"))
self.assertTrue(len(migration_files) > 0, "No migration file found")
migration_file = migration_files[0]
content = migration_file.read_text()
# Check that PolymorphicGuard is NOT in the migration file
self.assertNotIn(
"PolymorphicGuard", content, "Migration file should not contain PolymorphicGuard"
)
# Check that on_delete handlers are properly serialized
self.assertIn("django.db.models.deletion.CASCADE", content)
self.assertIn("django.db.models.deletion.PROTECT", content)
self.assertIn("django.db.models.deletion.SET_NULL", content)
self.assertIn("django.db.models.deletion.SET_DEFAULT", content)
self.assertIn("django.db.models.deletion.DO_NOTHING", content)
self.assertIn("django.db.models.deletion.RESTRICT", content)
# Check that SET() with callable is properly serialized
self.assertIn("models.SET", content)
self.assertIn("get_default_related", content)
def test_migration_serialization_stability(self):
"""
Test that the migration file contains stable serialization.
This ensures that PolymorphicGuard doesn't cause migration churn
by verifying the migration was generated successfully in setUpClass.
"""
# The fact that we have a migration file and it contains the right
# serialization (tested in test_migration_file_content) proves
# that the serialization is stable. If it wasn't stable, the
# migration file would either fail to generate or contain
# PolymorphicGuard references.
migration_files = list(self.migrations_dir.glob("0001_*.py"))
self.assertEqual(len(migration_files), 1, "Should have exactly one initial migration file")
def test_PolymorphicGuard_unwraps_correctly(self):
"""Test that PolymorphicGuard properly unwraps to the underlying action"""
from .models import ModelWithCascade
field = ModelWithCascade._meta.get_field("related")
on_delete = field.remote_field.on_delete
# Verify it's wrapped
self.assertIsInstance(on_delete, PolymorphicGuard)
# Verify the underlying action is CASCADE
self.assertEqual(on_delete.action, models.CASCADE)
def test_all_on_delete_types_covered(self):
"""
Meta-test to ensure we've covered all Django on_delete types.
This test documents which on_delete types we're testing.
"""
tested_types = {
"CASCADE": models.CASCADE,
"PROTECT": models.PROTECT,
"SET_NULL": models.SET_NULL,
"SET_DEFAULT": models.SET_DEFAULT,
"SET": models.SET, # This is a callable that returns the actual handler
"DO_NOTHING": models.DO_NOTHING,
"RESTRICT": models.RESTRICT,
}
# Document that we have test models for each type
from .models import (
ModelWithCascade,
ModelWithProtect,
ModelWithSetNull,
ModelWithSetDefault,
ModelWithSet,
ModelWithDoNothing,
ModelWithRestrict,
)
model_mapping = {
"CASCADE": ModelWithCascade,
"PROTECT": ModelWithProtect,
"SET_NULL": ModelWithSetNull,
"SET_DEFAULT": ModelWithSetDefault,
"SET": ModelWithSet,
"DO_NOTHING": ModelWithDoNothing,
"RESTRICT": ModelWithRestrict,
}
# Verify we have a model for each on_delete type
for type_name, on_delete_handler in tested_types.items():
with self.subTest(type=type_name):
self.assertIn(type_name, model_mapping, f"Missing test model for {type_name}")
model_class = model_mapping[type_name]
field = model_class._meta.get_field("related")
# Verify the field is properly configured
self.assertIsNotNone(field)
self.assertIsInstance(field.remote_field.on_delete, PolymorphicGuard)
class PolymorphicInheritanceSerializationTest(TestCase):
"""
Test that PolymorphicGuard works correctly with polymorphic model inheritance.
"""
def test_polymorphic_inheritance_chain(self):
"""Test that polymorphic model inheritance works with all on_delete types"""
from .models import BasePolyModel, ChildPolyModel, GrandChildPolyModel
# Verify the inheritance chain is set up correctly
self.assertTrue(issubclass(ChildPolyModel, BasePolyModel))
self.assertTrue(issubclass(GrandChildPolyModel, ChildPolyModel))
self.assertTrue(issubclass(GrandChildPolyModel, BasePolyModel))
# Verify each model has the polymorphic_ctype field
for model_class in [BasePolyModel, ChildPolyModel, GrandChildPolyModel]:
with self.subTest(model=model_class.__name__):
ctype_field = model_class._meta.get_field("polymorphic_ctype")
self.assertIsNotNone(ctype_field)
# The polymorphic_ctype field uses CASCADE which should also be wrapped
self.assertIsInstance(ctype_field.remote_field.on_delete, PolymorphicGuard)
class OnDeleteBehaviorTest(GeneratedMigrationsPerClassMixin, TransactionTestCase):
"""
Test that PolymorphicGuard correctly executes on_delete actions.
These tests verify the runtime behavior of each on_delete type when
wrapped with PolymorphicGuard by creating and deleting model instances.
"""
apps_to_migrate: list[str] = ["test_migrations"]
def test_cascade_deletes_related_objects(self):
"""Test that CASCADE deletes related polymorphic objects"""
from .models import RelatedModel, ModelWithCascade
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
cascade_obj = ModelWithCascade.objects.create(related=related)
cascade_obj_id = cascade_obj.id
# Verify the object exists
self.assertTrue(ModelWithCascade.objects.filter(id=cascade_obj_id).exists())
# Delete the related model
related.delete()
# Verify the cascade object was deleted
self.assertFalse(ModelWithCascade.objects.filter(id=cascade_obj_id).exists())
def test_protect_prevents_deletion(self):
"""Test that PROTECT prevents deletion of related objects"""
from .models import RelatedModel, ModelWithProtect
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
ModelWithProtect.objects.create(related=related)
# Attempting to delete the related model should raise ProtectedError
with self.assertRaises(ProtectedError):
related.delete()
# Verify both objects still exist
self.assertTrue(RelatedModel.objects.filter(id=related.id).exists())
self.assertTrue(ModelWithProtect.objects.filter(related=related).exists())
def test_set_null_sets_field_to_null(self):
"""Test that SET_NULL sets the foreign key to null"""
from .models import RelatedModel, ModelWithSetNull
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
set_null_obj = ModelWithSetNull.objects.create(related=related)
set_null_obj_id = set_null_obj.id
# Verify the relationship exists
self.assertEqual(set_null_obj.related, related)
# Delete the related model
related.delete()
# Verify the object still exists but the field is now null
set_null_obj = ModelWithSetNull.objects.get(id=set_null_obj_id)
self.assertIsNone(set_null_obj.related)
def test_set_default_sets_field_to_default(self):
"""Test that SET_DEFAULT sets the foreign key to its default value"""
from .models import RelatedModel, ModelWithSetDefault
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
set_default_obj = ModelWithSetDefault.objects.create(related=related)
set_default_obj_id = set_default_obj.id
# Verify the relationship exists
self.assertEqual(set_default_obj.related, related)
# Delete the related model
related.delete()
# Verify the object still exists but the field is now set to default (None)
set_default_obj = ModelWithSetDefault.objects.get(id=set_default_obj_id)
self.assertIsNone(set_default_obj.related)
def test_set_callable_uses_function(self):
"""Test that SET(...) calls the provided function"""
from .models import RelatedModel, ModelWithSet
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
set_obj = ModelWithSet.objects.create(related=related)
set_obj_id = set_obj.id
# Verify the relationship exists
self.assertEqual(set_obj.related, related)
# Delete the related model
related.delete()
# Verify the object s
set_obj = ModelWithSet.objects.get(id=set_obj_id)
self.assertIsNone(set_obj.related)
def test_do_nothing_behavior(self):
"""Test that DO_NOTHING doesn't prevent deletion or update related objects"""
from .models import RelatedModel, ModelWithDoNothing
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
do_nothing_obj = ModelWithDoNothing.objects.create(related=related)
# Verify the object is wrapped with PolymorphicGuard
field = ModelWithDoNothing._meta.get_field("related")
self.assertIsInstance(field.remote_field.on_delete, PolymorphicGuard)
# Verify the underlying action is DO_NOTHING
self.assertEqual(field.remote_field.on_delete.action, models.DO_NOTHING)
# DO_NOTHING doesn't cascade delete or set null - it simply does nothing
# In practice, this means the deletion succeeds but leaves an orphaned reference
# However, database constraints may prevent this in production
# Here we just verify that the wrapper is correct and the object exists
self.assertTrue(ModelWithDoNothing.objects.filter(id=do_nothing_obj.id).exists())
self.assertEqual(do_nothing_obj.related, related)
def test_restrict_prevents_deletion_when_objects_exist(self):
"""Test that RESTRICT prevents deletion when related objects exist"""
from .models import RelatedModel, ModelWithRestrict
# Create a related model and a polymorphic model that references it
related = RelatedModel.objects.create(name="test")
restrict_obj = ModelWithRestrict.objects.create(related=related)
# Attempting to delete the related model should raise RestrictedError
with self.assertRaises(RestrictedError):
related.delete()
# Verify both objects still exist
self.assertTrue(RelatedModel.objects.filter(id=related.id).exists())
self.assertTrue(ModelWithRestrict.objects.filter(id=restrict_obj.id).exists())
def test_cascade_with_polymorphic_inheritance(self):
"""Test CASCADE works correctly with polymorphic child models"""
from .models import RelatedModel, ModelWithCascade
# Create a related model
related = RelatedModel.objects.create(name="test")
# Create multiple instances of the polymorphic model
obj1 = ModelWithCascade.objects.create(related=related)
obj2 = ModelWithCascade.objects.create(related=related)
obj1_id, obj2_id = obj1.id, obj2.id
# Verify they exist
self.assertEqual(ModelWithCascade.objects.filter(related=related).count(), 2)
# Delete the related model
related.delete()
# Verify all cascade objects were deleted
self.assertFalse(ModelWithCascade.objects.filter(id=obj1_id).exists())
self.assertFalse(ModelWithCascade.objects.filter(id=obj2_id).exists())
self.assertEqual(ModelWithCascade.objects.count(), 0)
def test_one_to_one_cascade_deletes_related_object(self):
"""Test CASCADE with OneToOneField deletes related polymorphic object"""
from .models import RelatedModel, ModelWithOneToOneCascade
# Create a related model and a polymorphic model with OneToOne
related = RelatedModel.objects.create(name="test")
one_to_one_obj = ModelWithOneToOneCascade.objects.create(related=related)
one_to_one_obj_id = one_to_one_obj.id
# Verify the object exists
self.assertTrue(ModelWithOneToOneCascade.objects.filter(id=one_to_one_obj_id).exists())
# Delete the related model
related.delete()
# Verify the one-to-one object was deleted
self.assertFalse(ModelWithOneToOneCascade.objects.filter(id=one_to_one_obj_id).exists())
def test_one_to_one_protect_prevents_deletion(self):
"""Test PROTECT with OneToOneField prevents deletion"""
from .models import RelatedModel, ModelWithOneToOneProtect
# Create a related model and a polymorphic model with OneToOne
related = RelatedModel.objects.create(name="test")
ModelWithOneToOneProtect.objects.create(related=related)
# Attempting to delete should raise ProtectedError
with self.assertRaises(ProtectedError):
related.delete()
# Verify both objects still exist
self.assertTrue(RelatedModel.objects.filter(id=related.id).exists())
self.assertTrue(ModelWithOneToOneProtect.objects.filter(related=related).exists())
def test_one_to_one_set_null_sets_to_null(self):
"""Test SET_NULL with OneToOneField sets field to null"""
from .models import RelatedModel, ModelWithOneToOneSetNull
# Create a related model and a polymorphic model with OneToOne
related = RelatedModel.objects.create(name="test")
one_to_one_obj = ModelWithOneToOneSetNull.objects.create(related=related)
one_to_one_obj_id = one_to_one_obj.id
# Verify the relationship exists
self.assertEqual(one_to_one_obj.related, related)
# Delete the related model
related.delete()
# Verify the object still exists but the field is now null
one_to_one_obj = ModelWithOneToOneSetNull.objects.get(id=one_to_one_obj_id)
self.assertIsNone(one_to_one_obj.related)

View File

@ -1358,7 +1358,7 @@ class PolymorphicTests(TransactionTestCase):
assert False, "Unexpected model type"
assert (b, c, d) == (250, 1000, 2000)
assert len(poly_all) <= 7, (
assert len(poly_all) <= 8, (
f"Expected < 7 queries for chunked iteration over 3250 "
f"objects with 3 child models and the default chunk size of 2000, encountered "
f"{len(poly_all)}"
@ -1450,23 +1450,16 @@ class PolymorphicTests(TransactionTestCase):
if connection.vendor == "postgresql":
assert len(poly_chunked) == 4, "On postgres with a 4000 chunk size, expected 4 queries"
try:
result = Model2A.objects.all().delete()
assert result == (
11500,
{
"tests.Model2D": 2000,
"tests.Model2C": 3000,
"tests.Model2A": 3250,
"tests.Model2B": 3250,
},
)
except AttributeError:
if connection.vendor == "oracle":
# FIXME
# known deletion issue with oracle
# https://github.com/jazzband/django-polymorphic/issues/673
pass
result = Model2A.objects.all().delete()
assert result == (
11500,
{
"tests.Model2D": 2000,
"tests.Model2C": 3000,
"tests.Model2A": 3250,
"tests.Model2B": 3250,
},
)
def test_transmogrify_with_init(self):
pur = PurpleHeadDuck.objects.create()

View File

@ -0,0 +1,87 @@
import os
import shutil
from pathlib import Path
import io
from django.core.management import call_command
from django_test_migrations.migrator import Migrator
from django.apps import apps
class GeneratedMigrationsPerClassMixin:
"""
Generates migrations at class setup, applies them, and rolls them back at teardown.
Configure:
- apps_to_migrate = ["my_app", ...]
- database = "default" (optional)
"""
apps_to_migrate: list[str] = []
database: str = "default"
settings: str = os.environ.get("DJANGO_SETTINGS_MODULE", "polymorphic.tests.settings")
@classmethod
def setUpClass(cls):
super().setUpClass()
if not cls.apps_to_migrate:
raise RuntimeError("Set apps_to_migrate = ['your_app', ...]")
for app_label in cls.apps_to_migrate:
call_command(
"makemigrations",
app_label,
interactive=False,
verbosity=0,
)
# 2) Apply all migrations (up to latest) using django-test-migrations
cls.migrator = Migrator(database=cls.database)
cls._applied_states = {}
for app_label in cls.apps_to_migrate:
latest = cls._find_latest_migration_name(app_label)
# apply_initial_migration applies all migrations up to and including `latest`
cls._applied_states[app_label] = cls.migrator.apply_initial_migration(
(app_label, latest)
)
@classmethod
def tearDownClass(cls):
try:
# Roll everything back / cleanup:
if hasattr(cls, "migrator"):
cls.migrator.reset()
finally:
# remove files
for app_label in cls.apps_to_migrate:
app_config = apps.get_app_config(app_label) # app *label*
mig_dir = Path(app_config.path) / "migrations"
for mig_file in mig_dir.glob("*.py"):
if mig_file.name != "__init__.py" and mig_file.name[0:4].isdigit():
os.remove(mig_file)
# also remove __pycache__ if exists
pycache_dir = mig_dir / "__pycache__"
if pycache_dir.exists() and pycache_dir.is_dir():
shutil.rmtree(pycache_dir)
super().tearDownClass()
@classmethod
def _find_latest_migration_name(cls, app_label: str) -> str:
"""
Returns "000X_..." latest migration filename (without .py).
"""
app_config = apps.get_app_config(app_label) # app *label*
mig_dir = Path(app_config.path) / "migrations"
candidates = sorted(
p for p in mig_dir.glob("*.py") if p.name != "__init__.py" and p.name[0:4].isdigit()
)
if not candidates:
raise RuntimeError(f"No migrations generated for {app_label}")
return candidates[-1].stem

364
uv.lock
View File

@ -87,11 +87,11 @@ wheels = [
[[package]]
name = "cachetools"
version = "6.2.2"
version = "6.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" },
{ url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" },
]
[[package]]
@ -641,6 +641,7 @@ cx-oracle = [
dev = [
{ name = "coverage" },
{ name = "dj-database-url" },
{ name = "django-test-migrations" },
{ name = "ipdb" },
{ name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@ -661,7 +662,7 @@ docs = [
{ name = "furo" },
{ name = "readme-renderer", extra = ["md"] },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-django" },
@ -687,6 +688,7 @@ cx-oracle = [{ name = "cx-oracle", specifier = ">=8.3.0" }]
dev = [
{ name = "coverage", specifier = ">=7.6.1" },
{ name = "dj-database-url", specifier = ">=2.2.0" },
{ name = "django-test-migrations", specifier = ">=1.5.0" },
{ name = "ipdb", specifier = ">=0.13.13" },
{ name = "ipython", specifier = ">=8.18.1" },
{ name = "mypy", specifier = ">=1.14.1" },
@ -714,6 +716,18 @@ oracledb = [{ name = "oracledb", specifier = ">=2.3.0" }]
psycopg2 = [{ name = "psycopg2", specifier = ">=2.9.10" }]
psycopg3 = [{ name = "psycopg" }]
[[package]]
name = "django-test-migrations"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/ed/7fc6f8e89d83565fc4acb93ae0a2387d885ac83cda445cb6c570f302bf55/django_test_migrations-1.5.0.tar.gz", hash = "sha256:1cbff04b1e82c5564a6f635284907b381cc11a2ff883adff46776d9126824f07", size = 20143, upload-time = "2025-04-18T10:15:38.547Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/fe/38789c69f71adff9156bda7542d8fd05fcde1a109cf67bd7a1a139f8199f/django_test_migrations-1.5.0-py3-none-any.whl", hash = "sha256:96a08f085fc8bfaa53d44618341d82a2d22fd194c821cd81b147b66f0bec0da8", size = 25099, upload-time = "2025-04-18T10:15:37.16Z" },
]
[[package]]
name = "doc8"
version = "2.0.0"
@ -762,28 +776,28 @@ wheels = [
[[package]]
name = "filelock"
version = "3.20.0"
version = "3.20.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
]
[[package]]
name = "furo"
version = "2025.9.25"
version = "2025.12.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/29/ff3b83a1ffce74676043ab3e7540d398e0b1ce7660917a00d7c4958b93da/furo-2025.9.25.tar.gz", hash = "sha256:3eac05582768fdbbc2bdfa1cdbcdd5d33cfc8b4bd2051729ff4e026a1d7e0a98", size = 1662007, upload-time = "2025-09-25T21:37:19.221Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/69/964b55f389c289e16ba2a5dfe587c3c462aac09e24123f09ddf703889584/furo-2025.9.25-py3-none-any.whl", hash = "sha256:2937f68e823b8e37b410c972c371bc2b1d88026709534927158e0cb3fac95afe", size = 340409, upload-time = "2025-09-25T21:37:17.244Z" },
{ url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" },
]
[[package]]
@ -990,75 +1004,75 @@ wheels = [
[[package]]
name = "librt"
version = "0.7.3"
version = "0.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" }
sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/66/79a14e672256ef58144a24eb49adb338ec02de67ff4b45320af6504682ab/librt-0.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2682162855a708e3270eba4b92026b93f8257c3e65278b456c77631faf0f4f7a", size = 54707, upload-time = "2025-12-06T19:03:10.881Z" },
{ url = "https://files.pythonhosted.org/packages/58/fa/b709c65a9d5eab85f7bcfe0414504d9775aaad6e78727a0327e175474caa/librt-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:440c788f707c061d237c1e83edf6164ff19f5c0f823a3bf054e88804ebf971ec", size = 56670, upload-time = "2025-12-06T19:03:12.107Z" },
{ url = "https://files.pythonhosted.org/packages/3a/56/0685a0772ec89ddad4c00e6b584603274c3d818f9a68e2c43c4eb7b39ee9/librt-0.7.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399938edbd3d78339f797d685142dd8a623dfaded023cf451033c85955e4838a", size = 161045, upload-time = "2025-12-06T19:03:13.444Z" },
{ url = "https://files.pythonhosted.org/packages/4e/d9/863ada0c5ce48aefb89df1555e392b2209fcb6daee4c153c031339b9a89b/librt-0.7.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1975eda520957c6e0eb52d12968dd3609ffb7eef05d4223d097893d6daf1d8a7", size = 169532, upload-time = "2025-12-06T19:03:14.699Z" },
{ url = "https://files.pythonhosted.org/packages/68/a0/71da6c8724fd16c31749905ef1c9e11de206d9301b5be984bf2682b4efb3/librt-0.7.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9da128d0edf990cf0d2ca011b02cd6f639e79286774bd5b0351245cbb5a6e51", size = 183277, upload-time = "2025-12-06T19:03:16.446Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/9c97bf2f8338ba1914de233ea312bba2bbd7c59f43f807b3e119796bab18/librt-0.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19acfde38cb532a560b98f473adc741c941b7a9bc90f7294bc273d08becb58b", size = 179045, upload-time = "2025-12-06T19:03:17.838Z" },
{ url = "https://files.pythonhosted.org/packages/b3/b1/ceea067f489e904cb4ddcca3c9b06ba20229bc3fa7458711e24a5811f162/librt-0.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b4f57f7a0c65821c5441d98c47ff7c01d359b1e12328219709bdd97fdd37f90", size = 173521, upload-time = "2025-12-06T19:03:19.17Z" },
{ url = "https://files.pythonhosted.org/packages/7a/41/6cb18f5da9c89ed087417abb0127a445a50ad4eaf1282ba5b52588187f47/librt-0.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:256793988bff98040de23c57cf36e1f4c2f2dc3dcd17537cdac031d3b681db71", size = 193592, upload-time = "2025-12-06T19:03:20.637Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3c/fcef208746584e7c78584b7aedc617130c4a4742cb8273361bbda8b183b5/librt-0.7.3-cp310-cp310-win32.whl", hash = "sha256:fcb72249ac4ea81a7baefcbff74df7029c3cb1cf01a711113fa052d563639c9c", size = 47201, upload-time = "2025-12-06T19:03:21.764Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bf/d8a6c35d1b2b789a4df9b3ddb1c8f535ea373fde2089698965a8f0d62138/librt-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:4887c29cadbdc50640179e3861c276325ff2986791e6044f73136e6e798ff806", size = 54371, upload-time = "2025-12-06T19:03:23.231Z" },
{ url = "https://files.pythonhosted.org/packages/21/e6/f6391f5c6f158d31ed9af6bd1b1bcd3ffafdea1d816bc4219d0d90175a7f/librt-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af", size = 54711, upload-time = "2025-12-06T19:03:24.6Z" },
{ url = "https://files.pythonhosted.org/packages/ab/1b/53c208188c178987c081560a0fcf36f5ca500d5e21769596c845ef2f40d4/librt-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5", size = 56664, upload-time = "2025-12-06T19:03:25.969Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5c/d9da832b9a1e5f8366e8a044ec80217945385b26cb89fd6f94bfdc7d80b0/librt-0.7.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7", size = 161701, upload-time = "2025-12-06T19:03:27.035Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/1e0a7aba15e78529dd21f233076b876ee58c8b8711b1793315bdd3b263b0/librt-0.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445", size = 171040, upload-time = "2025-12-06T19:03:28.482Z" },
{ url = "https://files.pythonhosted.org/packages/69/46/3cfa325c1c2bc25775ec6ec1718cfbec9cff4ac767d37d2d3a2d1cc6f02c/librt-0.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7", size = 184720, upload-time = "2025-12-06T19:03:29.599Z" },
{ url = "https://files.pythonhosted.org/packages/99/bb/e4553433d7ac47f4c75d0a7e59b13aee0e08e88ceadbee356527a9629b0a/librt-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b", size = 180731, upload-time = "2025-12-06T19:03:31.201Z" },
{ url = "https://files.pythonhosted.org/packages/35/89/51cd73006232981a3106d4081fbaa584ac4e27b49bc02266468d3919db03/librt-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720", size = 174565, upload-time = "2025-12-06T19:03:32.818Z" },
{ url = "https://files.pythonhosted.org/packages/42/54/0578a78b587e5aa22486af34239a052c6366835b55fc307bc64380229e3f/librt-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a", size = 195247, upload-time = "2025-12-06T19:03:34.434Z" },
{ url = "https://files.pythonhosted.org/packages/b5/0a/ee747cd999753dd9447e50b98fc36ee433b6c841a42dbf6d47b64b32a56e/librt-0.7.3-cp311-cp311-win32.whl", hash = "sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2", size = 47514, upload-time = "2025-12-06T19:03:35.959Z" },
{ url = "https://files.pythonhosted.org/packages/ec/af/8b13845178dec488e752878f8e290f8f89e7e34ae1528b70277aa1a6dd1e/librt-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e", size = 54695, upload-time = "2025-12-06T19:03:36.956Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/ae59578501b1a25850266778f59279f4f3e726acc5c44255bfcb07b4bc57/librt-0.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0", size = 48142, upload-time = "2025-12-06T19:03:38.263Z" },
{ url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" },
{ url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" },
{ url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" },
{ url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" },
{ url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" },
{ url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" },
{ url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" },
{ url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" },
{ url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" },
{ url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" },
{ url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" },
{ url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" },
{ url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" },
{ url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" },
{ url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" },
{ url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" },
{ url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" },
{ url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" },
{ url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" },
{ url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" },
{ url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" },
{ url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" },
{ url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" },
{ url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" },
{ url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" },
{ url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" },
{ url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" },
{ url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" },
{ url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" },
{ url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" },
{ url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" },
{ url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" },
{ url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" },
{ url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" },
{ url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" },
{ url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" },
{ url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" },
{ url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" },
{ url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/3e61dff6c07a3b400fe907d3164b92b3b3023ef86eac1ee236869dc276f7/librt-0.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc300cb5a5a01947b1ee8099233156fdccd5001739e5f596ecfbc0dab07b5a3b", size = 54708, upload-time = "2025-12-15T16:51:03.752Z" },
{ url = "https://files.pythonhosted.org/packages/87/98/ab2428b0a80d0fd67decaeea84a5ec920e3dd4d95ecfd074c71f51bd7315/librt-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee8d3323d921e0f6919918a97f9b5445a7dfe647270b2629ec1008aa676c0bc0", size = 56656, upload-time = "2025-12-15T16:51:05.038Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ce/de1fad3a16e4fb5b6605bd6cbe6d0e5207cc8eca58993835749a1da0812b/librt-0.7.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:95cb80854a355b284c55f79674f6187cc9574df4dc362524e0cce98c89ee8331", size = 161024, upload-time = "2025-12-15T16:51:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/88/00/ddfcdc1147dd7fb68321d7b064b12f0b9101d85f466a46006f86096fde8d/librt-0.7.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca1caedf8331d8ad6027f93b52d68ed8f8009f5c420c246a46fe9d3be06be0f", size = 169529, upload-time = "2025-12-15T16:51:07.907Z" },
{ url = "https://files.pythonhosted.org/packages/dd/b3/915702c7077df2483b015030d1979404474f490fe9a071e9576f7b26fef6/librt-0.7.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a6f1236151e6fe1da289351b5b5bce49651c91554ecc7b70a947bced6fe212", size = 183270, upload-time = "2025-12-15T16:51:09.164Z" },
{ url = "https://files.pythonhosted.org/packages/45/19/ab2f217e8ec509fca4ea9e2e5022b9f72c1a7b7195f5a5770d299df807ea/librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783", size = 179038, upload-time = "2025-12-15T16:51:10.538Z" },
{ url = "https://files.pythonhosted.org/packages/10/1c/d40851d187662cf50312ebbc0b277c7478dd78dbaaf5ee94056f1d7f2f83/librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979", size = 173502, upload-time = "2025-12-15T16:51:11.888Z" },
{ url = "https://files.pythonhosted.org/packages/07/52/d5880835c772b22c38db18660420fa6901fd9e9a433b65f0ba9b0f4da764/librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3", size = 193570, upload-time = "2025-12-15T16:51:13.168Z" },
{ url = "https://files.pythonhosted.org/packages/f1/35/22d3c424b82f86ce019c0addadf001d459dfac8036aecc07fadc5c541053/librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997", size = 42596, upload-time = "2025-12-15T16:51:14.422Z" },
{ url = "https://files.pythonhosted.org/packages/95/b1/e7c316ac5fe60ac1fdfe515198087205220803c4cf923ee63e1cb8380b17/librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8", size = 48972, upload-time = "2025-12-15T16:51:15.516Z" },
{ url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" },
{ url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" },
{ url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" },
{ url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" },
{ url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" },
{ url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" },
{ url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" },
{ url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" },
{ url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" },
{ url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" },
{ url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" },
{ url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" },
{ url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" },
{ url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" },
{ url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" },
{ url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" },
{ url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" },
{ url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" },
{ url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" },
{ url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" },
{ url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" },
{ url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" },
{ url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" },
{ url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" },
{ url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" },
{ url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" },
{ url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" },
{ url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" },
{ url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" },
{ url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" },
{ url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" },
{ url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" },
{ url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" },
{ url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" },
{ url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" },
{ url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" },
{ url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" },
{ url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" },
{ url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" },
{ url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" },
{ url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" },
{ url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" },
{ url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" },
{ url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" },
]
[[package]]
@ -1160,48 +1174,48 @@ wheels = [
[[package]]
name = "mypy"
version = "1.19.0"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt" },
{ 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/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" }
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/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" },
{ url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" },
{ url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" },
{ url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" },
{ url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" },
{ url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" },
{ url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" },
{ url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" },
{ url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" },
{ url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" },
{ url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" },
{ url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" },
{ url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" },
{ url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" },
{ url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" },
{ url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" },
{ url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" },
{ url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" },
{ url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" },
{ url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" },
{ url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" },
{ url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" },
{ url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" },
{ url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" },
{ url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" },
{ url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" },
{ url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" },
{ url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" },
{ url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" },
{ url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" },
{ 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]]
@ -1392,7 +1406,7 @@ wheels = [
[[package]]
name = "pre-commit"
version = "4.5.0"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@ -1401,9 +1415,9 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
[[package]]
@ -1700,38 +1714,38 @@ wheels = [
]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
name = "roman-numerals"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
{ url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" },
]
[[package]]
name = "ruff"
version = "0.14.9"
version = "0.14.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" }
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" },
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" },
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" },
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" },
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" },
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" },
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" },
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" },
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" },
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" },
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" },
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
[[package]]
@ -1745,11 +1759,11 @@ wheels = [
[[package]]
name = "soupsieve"
version = "2.8"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
{ url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" },
]
[[package]]
@ -1785,7 +1799,7 @@ wheels = [
[[package]]
name = "sphinx"
version = "8.2.3"
version = "9.0.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
@ -1801,7 +1815,7 @@ dependencies = [
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pygments", marker = "python_full_version >= '3.11'" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
{ name = "roman-numerals-py", marker = "python_full_version >= '3.11'" },
{ name = "roman-numerals", marker = "python_full_version >= '3.11'" },
{ name = "snowballstemmer", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" },
@ -1810,9 +1824,9 @@ dependencies = [
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
{ url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" },
]
[[package]]
@ -1845,7 +1859,7 @@ resolution-markers = [
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" },
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
{ name = "watchfiles", marker = "python_full_version >= '3.11'" },
@ -1862,7 +1876,7 @@ version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
wheels = [
@ -1896,7 +1910,7 @@ dependencies = [
{ name = "django", version = "6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "pprintpp" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/28/92f6d685899fbd74a6c575c50dcc1abb8ab69c6da0160bc99d557d2104d1/sphinxcontrib-django-2.5.tar.gz", hash = "sha256:45a54c0cc1f641d6c15872828862f0738348ca8d7d5b92777bcaa530678c2cc4", size = 23788, upload-time = "2023-09-26T17:54:36.259Z" }
wheels = [
@ -1941,11 +1955,11 @@ wheels = [
[[package]]
name = "sqlparse"
version = "0.5.4"
version = "0.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112, upload-time = "2025-11-28T07:10:18.377Z" }
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/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" },
{ 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]]
@ -2108,11 +2122,11 @@ wheels = [
[[package]]
name = "tzdata"
version = "2025.2"
version = "2025.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
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/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
{ 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]]
@ -2126,28 +2140,28 @@ wheels = [
[[package]]
name = "uv"
version = "0.9.17"
version = "0.9.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/1a/cb0c37ae8513b253bcbc13d42392feb7d95ea696eb398b37535a28df9040/uv-0.9.17.tar.gz", hash = "sha256:6d93ab9012673e82039cfa7f9f66f69b388bc3f910f9e8a2ebee211353f620aa", size = 3815957, upload-time = "2025-12-09T23:01:21.756Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/03/1afff9e6362dc9d3a9e03743da0a4b4c7a0809f859c79eb52bbae31ea582/uv-0.9.18.tar.gz", hash = "sha256:17b5502f7689c4dc1fdeee9d8437a9a6664dcaa8476e70046b5f4753559533f5", size = 3824466, upload-time = "2025-12-16T15:45:11.81Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/e2/b6e2d473bdc37f4d86307151b53c0776e9925de7376ce297e92eab2e8894/uv-0.9.17-py3-none-linux_armv6l.whl", hash = "sha256:c708e6560ae5bc3cda1ba93f0094148ce773b6764240ced433acf88879e57a67", size = 21254511, upload-time = "2025-12-09T23:00:36.604Z" },
{ url = "https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10", size = 20384366, upload-time = "2025-12-09T23:01:17.293Z" },
{ url = "https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef", size = 18924624, upload-time = "2025-12-09T23:01:10.291Z" },
{ url = "https://files.pythonhosted.org/packages/21/56/9daf8bbe4a9a36eb0b9257cf5e1e20f9433d0ce996778ccf1929cbe071a4/uv-0.9.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8f283488bbcf19754910cc1ae7349c567918d6367c596e5a75d4751e0080eee0", size = 20671687, upload-time = "2025-12-09T23:00:51.927Z" },
{ url = "https://files.pythonhosted.org/packages/9f/c8/4050ff7dc692770092042fcef57223b8852662544f5981a7f6cac8fc488d/uv-0.9.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf8052ba669dc17bdba75dae655094d820f4044990ea95c01ec9688c182f1da", size = 20861866, upload-time = "2025-12-09T23:01:12.555Z" },
{ url = "https://files.pythonhosted.org/packages/84/d4/208e62b7db7a65cb3390a11604c59937e387d07ed9f8b63b54edb55e2292/uv-0.9.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06749461b11175a884be193120044e7f632a55e2624d9203398808907d346aad", size = 21858420, upload-time = "2025-12-09T23:01:00.009Z" },
{ url = "https://files.pythonhosted.org/packages/86/2c/91288cd5a04db37dfc1e0dad26ead84787db5832d9836b4cc8e0fa7f3c53/uv-0.9.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:35eb1a519688209160e48e1bb8032d36d285948a13b4dd21afe7ec36dc2a9787", size = 23471658, upload-time = "2025-12-09T23:00:49.503Z" },
{ url = "https://files.pythonhosted.org/packages/44/ba/493eba650ffad1df9e04fd8eabfc2d0aebc23e8f378acaaee9d95ca43518/uv-0.9.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb60a533e82690ab17dfe619ff7f294d053415645800d38d13062170230714", size = 23062950, upload-time = "2025-12-09T23:00:39.055Z" },
{ url = "https://files.pythonhosted.org/packages/9a/9e/f7f679503c06843ba59451e3193f35fb7c782ff0afc697020d4718a7de46/uv-0.9.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd0f3e380ff148aff3d769e95a9743cb29c7f040d7ef2896cafe8063279a6bc1", size = 22080299, upload-time = "2025-12-09T23:00:44.026Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616", size = 22087554, upload-time = "2025-12-09T23:00:41.715Z" },
{ url = "https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2", size = 20823225, upload-time = "2025-12-09T23:00:57.041Z" },
{ url = "https://files.pythonhosted.org/packages/11/73/e0f816cacd802a1cb25e71de9d60e57fa1f6c659eb5599cef708668618cc/uv-0.9.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:45880faa9f6cf91e3cda4e5f947da6a1004238fdc0ed4ebc18783a12ce197312", size = 22004893, upload-time = "2025-12-09T23:01:15.011Z" },
{ url = "https://files.pythonhosted.org/packages/15/6b/700f6256ee191136eb06e40d16970a4fc687efdccf5e67c553a258063019/uv-0.9.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8e775a1b94c6f248e22f0ce2f86ed37c24e10ae31fb98b7e1b9f9a3189d25991", size = 20853850, upload-time = "2025-12-09T23:01:02.694Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6a/13f02e2ed6510223c40f74804586b09e5151d9319f93aab1e49d91db13bb/uv-0.9.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8650c894401ec96488a6fd84a5b4675e09be102f5525c902a12ba1c8ef8ff230", size = 21322623, upload-time = "2025-12-09T23:00:46.806Z" },
{ url = "https://files.pythonhosted.org/packages/d0/18/2d19780cebfbec877ea645463410c17859f8070f79c1a34568b153d78e1d/uv-0.9.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:673066b72d8b6c86be0dae6d5f73926bcee8e4810f1690d7b8ce5429d919cde3", size = 22290123, upload-time = "2025-12-09T23:00:54.394Z" },
{ url = "https://files.pythonhosted.org/packages/77/69/ab79bde3f7b6d2ac89f839ea40411a9cf3e67abede2278806305b6ba797e/uv-0.9.17-py3-none-win32.whl", hash = "sha256:7407d45afeae12399de048f7c8c2256546899c94bd7892dbddfae6766616f5a3", size = 20070709, upload-time = "2025-12-09T23:01:05.105Z" },
{ url = "https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl", hash = "sha256:22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b", size = 22122852, upload-time = "2025-12-09T23:01:07.783Z" },
{ url = "https://files.pythonhosted.org/packages/37/ef/813cfedda3c8e49d8b59a41c14fcc652174facfd7a1caf9fee162b40ccbd/uv-0.9.17-py3-none-win_arm64.whl", hash = "sha256:6761076b27a763d0ede2f5e72455d2a46968ff334badf8312bb35988c5254831", size = 20435751, upload-time = "2025-12-09T23:01:19.732Z" },
{ url = "https://files.pythonhosted.org/packages/26/9c/92fad10fcee8ea170b66442d95fd2af308fe9a107909ded4b3cc384fdc69/uv-0.9.18-py3-none-linux_armv6l.whl", hash = "sha256:e9e4915bb280c1f79b9a1c16021e79f61ed7c6382856ceaa99d53258cb0b4951", size = 21345538, upload-time = "2025-12-16T15:45:13.992Z" },
{ url = "https://files.pythonhosted.org/packages/81/b1/b0e5808e05acb54aa118c625d9f7b117df614703b0cbb89d419d03d117f3/uv-0.9.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d91abfd2649987996e3778729140c305ef0f6ff5909f55aac35c3c372544a24f", size = 20439572, upload-time = "2025-12-16T15:45:26.397Z" },
{ url = "https://files.pythonhosted.org/packages/b7/0b/9487d83adf5b7fd1e20ced33f78adf84cb18239c3d7e91f224cedba46c08/uv-0.9.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cf33f4146fd97e94cdebe6afc5122208eea8c55b65ca4127f5a5643c9717c8b8", size = 18952907, upload-time = "2025-12-16T15:44:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/58/92/c8f7ae8900eff8e4ce1f7826d2e1e2ad5a95a5f141abdb539865aff79930/uv-0.9.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:edf965e9a5c55f74020ac82285eb0dfe7fac4f325ad0a7afc816290269ecfec1", size = 20772495, upload-time = "2025-12-16T15:45:29.614Z" },
{ url = "https://files.pythonhosted.org/packages/5a/28/9831500317c1dd6cde5099e3eb3b22b88ac75e47df7b502f6aef4df5750e/uv-0.9.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae10a941bd7ca1ee69edbe3998c34dce0a9fc2d2406d98198343daf7d2078493", size = 20949623, upload-time = "2025-12-16T15:44:57.482Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ff/1fe1ffa69c8910e54dd11f01fb0765d4fd537ceaeb0c05fa584b6b635b82/uv-0.9.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1669a95b588f613b13dd10e08ced6d5bcd79169bba29a2240eee87532648790", size = 21920580, upload-time = "2025-12-16T15:44:39.009Z" },
{ url = "https://files.pythonhosted.org/packages/d6/ee/eed3ec7679ee80e16316cfc95ed28ef6851700bcc66edacfc583cbd2cc47/uv-0.9.18-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:11e1e406590d3159138288203a41ff8a8904600b8628a57462f04ff87d62c477", size = 23491234, upload-time = "2025-12-16T15:45:32.59Z" },
{ url = "https://files.pythonhosted.org/packages/78/58/64b15df743c79ad03ea7fbcbd27b146ba16a116c57f557425dd4e44d6684/uv-0.9.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e82078d3c622cb4c60da87f156168ffa78b9911136db7ffeb8e5b0a040bf30e", size = 23095438, upload-time = "2025-12-16T15:45:17.916Z" },
{ url = "https://files.pythonhosted.org/packages/43/6d/3d3dae71796961603c3871699e10d6b9de2e65a3c327b58d4750610a5f93/uv-0.9.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704abaf6e76b4d293fc1f24bef2c289021f1df0de9ed351f476cbbf67a7edae0", size = 22140992, upload-time = "2025-12-16T15:44:45.527Z" },
{ url = "https://files.pythonhosted.org/packages/31/91/1042d0966a30e937df500daed63e1f61018714406ce4023c8a6e6d2dcf7c/uv-0.9.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3332188fd8d96a68e5001409a52156dced910bf1bc41ec3066534cffcd46eb68", size = 22229626, upload-time = "2025-12-16T15:45:20.712Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1f/0a4a979bb2bf6e1292cc57882955bf1d7757cad40b1862d524c59c2a2ad8/uv-0.9.18-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:b7295e6d505f1fd61c54b1219e3b18e11907396333a9fa61cefe489c08fc7995", size = 20896524, upload-time = "2025-12-16T15:45:06.799Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3c/24f92e56af00cac7d9bed2888d99a580f8093c8745395ccf6213bfccf20b/uv-0.9.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:62ea0e518dd4ab76e6f06c0f43a25898a6342a3ecf996c12f27f08eb801ef7f1", size = 22077340, upload-time = "2025-12-16T15:44:51.271Z" },
{ url = "https://files.pythonhosted.org/packages/9c/3e/73163116f748800e676bf30cee838448e74ac4cc2f716c750e1705bc3fe4/uv-0.9.18-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8bd073e30030211ba01206caa57b4d63714e1adee2c76a1678987dd52f72d44d", size = 20932956, upload-time = "2025-12-16T15:45:00.3Z" },
{ url = "https://files.pythonhosted.org/packages/59/1b/a26990b51a17de1ffe41fbf2e30de3a98f0e0bce40cc60829fb9d9ed1a8a/uv-0.9.18-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f248e013d10e1fc7a41f94310628b4a8130886b6d683c7c85c42b5b36d1bcd02", size = 21357247, upload-time = "2025-12-16T15:45:23.575Z" },
{ url = "https://files.pythonhosted.org/packages/5f/20/b6ba14fdd671e9237b22060d7422aba4a34503e3e42d914dbf925eff19aa/uv-0.9.18-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:17bedf2b0791e87d889e1c7f125bd5de77e4b7579aec372fa06ba832e07c957e", size = 22443585, upload-time = "2025-12-16T15:44:42.213Z" },
{ url = "https://files.pythonhosted.org/packages/5e/da/1b3dd596964f90a122cfe94dcf5b6b89cf5670eb84434b8c23864382576f/uv-0.9.18-py3-none-win32.whl", hash = "sha256:de6f0bb3e9c18e484545bd1549ec3c956968a141a393d42e2efb25281cb62787", size = 20091088, upload-time = "2025-12-16T15:45:03.225Z" },
{ url = "https://files.pythonhosted.org/packages/11/0b/50e13ebc1eedb36d88524b7740f78351be33213073e3faf81ac8925d0c6e/uv-0.9.18-py3-none-win_amd64.whl", hash = "sha256:c82b0e2e36b33e2146fba5f0ae6906b9679b3b5fe6a712e5d624e45e441e58e9", size = 22181193, upload-time = "2025-12-16T15:44:54.394Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d4/0bf338d863a3d9e5545e268d77a8e6afdd75d26bffc939603042f2e739f9/uv-0.9.18-py3-none-win_arm64.whl", hash = "sha256:4c4ce0ed080440bbda2377488575d426867f94f5922323af6d4728a1cd4d091d", size = 20564933, upload-time = "2025-12-16T15:45:09.819Z" },
]
[[package]]