From 2690f18e9abdd59de73a19f3cb5b6103f331b814 Mon Sep 17 00:00:00 2001 From: David Reed Date: Mon, 22 Sep 2025 13:27:25 -0600 Subject: [PATCH 01/26] Game counts on Applications view and in Crew Builder --- stave/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stave/views.py b/stave/views.py index 7e33aa7..856e5b4 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2055,7 +2055,7 @@ def get( game = None applications = am.get_available_applications(crew, game, role) game_counts = { - a.user.id: am.get_game_count_for_user(a.user) for a in applications + a.user.id: am.get_game_count_for_user(a.user) for a in all_applications } # TODO: get the Game from AM to reduce queries. From b1ec128832aa9109e9120b040c3252291a2f3dc9 Mon Sep 17 00:00:00 2001 From: David Reed Date: Mon, 22 Sep 2025 21:52:51 -0600 Subject: [PATCH 02/26] Fix reference --- stave/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stave/views.py b/stave/views.py index 856e5b4..7e33aa7 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2055,7 +2055,7 @@ def get( game = None applications = am.get_available_applications(crew, game, role) game_counts = { - a.user.id: am.get_game_count_for_user(a.user) for a in all_applications + a.user.id: am.get_game_count_for_user(a.user) for a in applications } # TODO: get the Game from AM to reduce queries. From 4d2865d20957fee613217dbd631122e6eefc55ab Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 25 Sep 2025 07:59:51 -0600 Subject: [PATCH 03/26] Show unavail applicants --- stave/templates/stave/contexts.py | 1 + .../templates/stave/crew_builder_detail.html | 26 +++++++++++++++++++ stave/views.py | 6 +++++ 3 files changed, 33 insertions(+) diff --git a/stave/templates/stave/contexts.py b/stave/templates/stave/contexts.py index 139929f..6c24283 100644 --- a/stave/templates/stave/contexts.py +++ b/stave/templates/stave/contexts.py @@ -53,6 +53,7 @@ class CrewBuilderDetailInputs: role: models.Role game: models.Game | None applications: list[models.Application] + unavail_applications: list[models.Application] game_counts: dict[UUID, int] diff --git a/stave/templates/stave/crew_builder_detail.html b/stave/templates/stave/crew_builder_detail.html index 6c0a95a..e9e483e 100644 --- a/stave/templates/stave/crew_builder_detail.html +++ b/stave/templates/stave/crew_builder_detail.html @@ -37,5 +37,31 @@

+
+
+ Show {{ unavail_applications|length }} unavailable applicants + + {% include 'stave/partials/application_table_header.html' with form=form only %} + + {% for application in unavail_applications %} + + + {% with game_count=game_counts|get:application.user_id %} + {% include 'stave/partials/application_table_row.html' with form=form application=application game_count=game_count only %} + {% endwith %} + + {% endfor %} + +
+
+ {% csrf_token %} + + + + View +
+
+
+
{% endblock content %} diff --git a/stave/views.py b/stave/views.py index 7e33aa7..5d63ac6 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2054,6 +2054,11 @@ def get( else: game = None applications = am.get_available_applications(crew, game, role) + all_applications = am.get_all_applications(crew, game, role) + unavail_applications = [ + a for a in all_applications if a not in applications + ] # TODO: efficiency! + game_counts = { a.user.id: am.get_game_count_for_user(a.user) for a in applications } @@ -2066,6 +2071,7 @@ def get( contexts.CrewBuilderDetailInputs( form=application_form, applications=applications, + unavail_applications=unavail_applications, game=game, event=am.application_form.event, role=role, From 8aa76c6e72f6e70ee1cc2af806df41d5263e41b1 Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 20 Nov 2025 19:48:24 -0700 Subject: [PATCH 04/26] WIP --- .../templates/stave/crew_builder_detail.html | 9 +++---- stave/views.py | 24 +++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/stave/templates/stave/crew_builder_detail.html b/stave/templates/stave/crew_builder_detail.html index e9e483e..b42c470 100644 --- a/stave/templates/stave/crew_builder_detail.html +++ b/stave/templates/stave/crew_builder_detail.html @@ -37,9 +37,10 @@

+{% if unavail_applications %}
- Show {{ unavail_applications|length }} unavailable applicants + Show {{ unavail_applications|length }} unavailable applicant{% if unavail_applications|length != 1 %}s{% endif %} {% include 'stave/partials/application_table_header.html' with form=form only %} @@ -50,8 +51,8 @@

{% csrf_token %} - - View + + View {% with game_count=game_counts|get:application.user_id %} @@ -63,5 +64,5 @@

- +{% endif %} {% endblock content %} diff --git a/stave/views.py b/stave/views.py index 5d63ac6..362cef2 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2060,7 +2060,7 @@ def get( ] # TODO: efficiency! game_counts = { - a.user.id: am.get_game_count_for_user(a.user) for a in applications + a.user.id: am.get_game_count_for_user(a.user) for a in all_applications } # TODO: get the Game from AM to reduce queries. @@ -2117,7 +2117,7 @@ def post( else: applications = [] - # Delete existing assignment, if present + # Delete existing assignment in the target role, if present if assignment := models.CrewAssignment.objects.filter( role=role, crew=crew, @@ -2156,6 +2156,26 @@ def post( # Add a new assignment, if requested if applications: + # If the newly-selected user is already assigned to + # an exclusive role, AND that assignment is within + # one of this form's Role Groups, clear that assignment. + + # FIXME: we're deleting exclusive assignments when + # adding nonexclusive ones. + # FIXME: we aren't distinguishing between this form assignments + # and other-form assignments. + # FIXME: what if the user is on a static crew? We cannot override + # a static crew assignment to None + # TODO: display current assignment on crew builder detail + # TODO: show users who are time-available but not role-available + if assignments := models.CrewAssignment.objects.filter( + user=applications[0].user, + role__nonexclusive=False, + role__role_group__in=application_form.role_groups.all(), + crew__event=application_form.event, + ): + assignments.delete() + models.CrewAssignment.objects.create( role=role, crew=crew, user=applications[0].user ) From 089f1fde1c0c993eb52792f4436904704c54fca7 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 22 Nov 2025 23:49:53 -0700 Subject: [PATCH 05/26] WIP --- stave/avail.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stave/avail.py b/stave/avail.py index 81b6683..8c253df 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -333,6 +333,13 @@ def get_all_applications( self.applications_by_role_group[role.role_group_id][role.name], crew, game ) + @functools.cache + def get_swappable_applications(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> list[models.Application]: + """To be swappable, all overlapping assignments have to be in Role Groups managed on the same + Application Form, and cannot be part of a static crew. If we swapped out of a static crew, + we'd have no way to mark the slot empty.""" + ... + @functools.cache def get_available_applications( self, crew: models.Crew, game: models.Game | None, role: models.Role From ac0c260ecbb89a385eb1feb96fea31146d3797fe Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 27 Nov 2025 12:01:43 -0700 Subject: [PATCH 06/26] WIP --- stave/avail.py | 126 +++++++--- .../0056_alter_crewassignment_user.py | 28 +++ stave/models.py | 38 ++- .../templates/stave/partials/crew_editor.html | 9 +- stave/views.py | 219 ++++++++---------- 5 files changed, 258 insertions(+), 162 deletions(-) create mode 100644 stave/migrations/0056_alter_crewassignment_user.py diff --git a/stave/avail.py b/stave/avail.py index 8c253df..9c9510e 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -4,11 +4,16 @@ from datetime import datetime from typing import Iterable from uuid import UUID +import enum from django.db.models import Prefetch, QuerySet from . import models +class ConflictKind(enum.Enum): + NONE = 1 + NON_SWAPPABLE_CONFLICT = 2 + SWAPPABLE_CONFLICT = 3 @dataclass class UserAvailabilityEntry: @@ -19,22 +24,35 @@ class UserAvailabilityEntry: end_time: datetime | None exclusive: bool - def overlaps(self, other: "UserAvailabilityEntry") -> bool: + def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[models.RoleGroup]) -> ConflictKind: if self.crew.kind != other.crew.kind: - return False + return ConflictKind.NONE if self.crew.id == other.crew.id: - return self.exclusive and other.exclusive + if self.exclusive and other.exclusive: + return ConflictKind.SWAPPABLE_CONFLICT match self.crew.kind: case models.CrewKind.OVERRIDE_CREW: time_overlap = (self.start_time < other.end_time) and ( self.end_time > other.start_time ) - return time_overlap + if time_overlap: + if self.crew.role_group in swappable_role_groups: + return ConflictKind.SWAPPABLE_CONFLICT + else: + return ConflictKind.NON_SWAPPABLE_CONFLICT + case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: - return True + return ConflictKind.SWAPPABLE_CONFLICT + + return ConflictKind.NONE +@dataclass +class AvailabilitySet: + available: list[models.Application] + swappable: list[models.Application] + unavailable: list[models.Application] class ScheduleManager: event: models.Event @@ -222,6 +240,11 @@ def applications_by_role_group( return applications + @property + @functools.cache + def role_groups(self) -> set[models.RoleGroup]: + return set(self.application_form.role_groups.all()) + @property @functools.cache def static_crews(self) -> list[models.Crew]: @@ -253,14 +276,15 @@ def user_availability(self) -> dict[UUID, list[UserAvailabilityEntry]]: # as part of the override crew. This ensures we # catch conflicts between the assigned static crew # and the overrides. - user_assigned_times_map[assignment.user_id].append( - UserAvailabilityEntry( - crew=rgca.crew_overrides, - start_time=game.start_time, - end_time=game.end_time, - exclusive=not assignment.role.nonexclusive, + if assignment.user_id: + user_assigned_times_map[assignment.user_id].append( + UserAvailabilityEntry( + crew=rgca.crew_overrides, + start_time=game.start_time, + end_time=game.end_time, + exclusive=not assignment.role.nonexclusive, + ) ) - ) return user_assigned_times_map @@ -334,24 +358,61 @@ def get_all_applications( ) @functools.cache - def get_swappable_applications(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> list[models.Application]: - """To be swappable, all overlapping assignments have to be in Role Groups managed on the same - Application Form, and cannot be part of a static crew. If we swapped out of a static crew, - we'd have no way to mark the slot empty.""" - ... + def get_applications_by_avail(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> AvailabilitySet: + apps = self.get_all_applications(crew, game, role) + avail = self._get_avail_data_for_crew_kind(crew.kind) + avail_set = AvailabilitySet() + for app in applications: + overlaps = set( + t.overlaps(entry, self.role_groups) for t in avail[app.user_id] + ) + if ConflictKind.NON_SWAPPABLE_CONFLICT in overlaps: + avail_set.unavailable.append(app) + elif ConflictKind.SWAPPABLE_CONFLICT in overlaps: + avail_set.swappable.append(app) + else: + avail_set.available.append(app) + + return avail_set + @functools.cache - def get_available_applications( + def get_swappable_assignments( self, crew: models.Crew, game: models.Game | None, role: models.Role - ) -> list[models.Application]: - return self._filter_for_already_assigned_users( - self.get_all_applications(crew, game, role), + ) -> list[models.CrewAssignment]: + avail = self._get_avail_data_for_crew_kind(crew.kind) + + entry = UserAvailabilityEntry( crew, - game, - role, + game.start_time if game else None, + game.end_time if game else None, + not role.nonexclusive, ) + # TODO: go from apps to crew assignments + return list( + filter( + lambda app: all( + t.overlaps(entry, self.role_groups) in [ConflictKind.SWAPPABLE_CONFLICT, ConflictKind.NONE] + for t in self._get_avail_data_for_crew_kind(crew.kind)[app.user_id] + ), + applications, + ) + ) + + # Filter methods MUST NOT hit the database - use only cached data + + def _get_avail_data_for_crew_kind(self, kind: models.CrewKind) -> dict[UUID, list[UserAvailabilityEntry]]: + match kind: + case models.CrewKind.OVERRIDE_CREW: + return self.user_availability + case models.CrewKind.EVENT_CREW: + return self.user_event_availability + case models.CrewKind.GAME_CREW: + return self.user_static_crew_availability + + def _filter_for_basic_availability( self, applications: Iterable[models.Application], @@ -390,6 +451,7 @@ def _filter_for_already_assigned_users( crew: models.Crew, game: models.Game | None, role: models.Role, + allowed_conflict_kinds: set[ConflictKind] ) -> Iterable[models.Application]: entry = UserAvailabilityEntry( crew, @@ -397,18 +459,14 @@ def _filter_for_already_assigned_users( game.end_time if game else None, not role.nonexclusive, ) - - match crew.kind: - case models.CrewKind.OVERRIDE_CREW: - avail = self.user_availability - case models.CrewKind.EVENT_CREW: - avail = self.user_event_availability - case models.CrewKind.GAME_CREW: - avail = self.user_static_crew_availability - - return list( + avail = self._get_avail_data_for_crew_kind(crew.kind) + for app in applications: + overlaps = set( + t.overlaps(entry, self.role_groups) for t in avail[app.user_id] + ) filter( - lambda app: not any(t.overlaps(entry) for t in avail[app.user_id]), + lambda app: all( + ), applications, ) ) diff --git a/stave/migrations/0056_alter_crewassignment_user.py b/stave/migrations/0056_alter_crewassignment_user.py new file mode 100644 index 0000000..843ee73 --- /dev/null +++ b/stave/migrations/0056_alter_crewassignment_user.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.3 on 2025-11-25 17:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "stave", + "0055_alter_applicationformtemplateassignment_application_form_template_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="crewassignment", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="crews", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/stave/models.py b/stave/models.py index e440b13..6bf6260 100644 --- a/stave/models.py +++ b/stave/models.py @@ -619,7 +619,9 @@ def concrete_for_user(self, user: User) -> models.QuerySet["CrewAssignment"]: class CrewAssignment(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) crew = models.ForeignKey(Crew, related_name="assignments", on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name="crews", on_delete=models.CASCADE) + user = models.ForeignKey( + User, related_name="crews", on_delete=models.CASCADE, null=True, blank=True + ) role = models.ForeignKey( Role, related_name="crew_assignments", on_delete=models.CASCADE ) @@ -1972,6 +1974,40 @@ def get_legal_state_changes(self, user: User) -> list[ApplicationStatus]: return states + def move_status_backwards_for_unassignment(self): + if not self.has_assignments(): + # Reset its status appropriately. + if self.status == ApplicationStatus.ASSIGNMENT_PENDING: + if ( + self.form.application_kind + == ApplicationKind.CONFIRM_THEN_ASSIGN + ): + self.status = ApplicationStatus.CONFIRMED + else: + self.status = ApplicationStatus.APPLIED + elif self.status == ApplicationStatus.INVITATION_PENDING: + self.status = ApplicationStatus.APPLIED + self.save() + + def move_status_forwards_for_assignment(self): + # Update the status of the application + if self.form.application_kind == ApplicationKind.ASSIGN_ONLY: + # Note that this sends apps in ASSIGNED status backwards, + # so they'll get an update email. + self.status = ApplicationStatus.ASSIGNMENT_PENDING + else: + # for CONFIRM_THEN_ASSIGN events, our status update depends on the current status as well. + match self.status: + case ApplicationStatus.APPLIED: + self.status = ApplicationStatus.INVITATION_PENDING + case ApplicationStatus.CONFIRMED: + self.status = ApplicationStatus.ASSIGNMENT_PENDING + case _: + # All other cases do not update. + pass + + self.save() + class ApplicationResponse(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/stave/templates/stave/partials/crew_editor.html b/stave/templates/stave/partials/crew_editor.html index 6b9af09..9e40b7e 100644 --- a/stave/templates/stave/partials/crew_editor.html +++ b/stave/templates/stave/partials/crew_editor.html @@ -18,7 +18,7 @@ {% if role.id not in crew_assignments %}class="highlighted-cell"{% endif %} {% if assignment and assignment.user_id == focus_user_id %}class="highlighted-cell-user"{% endif %} > - {% if assignment %} + {% if assignment and assignment.user %} {{ assignment.user.preferred_name }} {% else %} {% if editable %} @@ -33,8 +33,6 @@ {% with assignment=crew_assignments|get:role.id %} - {# You cannot clear an assignment if it's provided by an assigned crew #} - {% if assignment and assignment.crew.id == crew.id %}
@@ -45,11 +43,6 @@ {% csrf_token %} - {% else %} -
- {% if assignment %}{% if assignment.crew.id == crew.id %}🔄{% else %}⬇️{% endif %}{% else %}🔍{% endif %} - - {% endif %} {% endwith %}
diff --git a/stave/views.py b/stave/views.py index 362cef2..5a2de04 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2089,133 +2089,114 @@ def post( crew_id: UUID, role_id: UUID, ) -> HttpResponse: - application_id = request.POST.get("application_id") + with transaction.atomic(): + application_id = request.POST.get("application_id") + application_form: models.ApplicationForm = get_object_or_404( + models.ApplicationForm.objects.manageable(request.user), + slug=application_form_slug, + event__slug=event_slug, + event__league__slug=league, + ) + role = get_object_or_404( + models.Role.objects.filter( + role_group__in=application_form.role_groups.all() + ), + pk=role_id, + ) + crew = get_object_or_404( + application_form.event.crews.filter(role_group=role.role_group), + pk=crew_id, + ) + am = AvailabilityManager.with_application_form(application_form) + + if application_id: + applications = application_form.applications.filter( + id=application_id, + roles__name=role.name, + ).exclude(status=models.ApplicationStatus.WITHDRAWN) + if len(applications) != 1: + return HttpResponseBadRequest("invalid application_id") + else: + applications = [] - application_form: models.ApplicationForm = get_object_or_404( - models.ApplicationForm.objects.manageable(request.user), - slug=application_form_slug, - event__slug=event_slug, - event__league__slug=league, - ) - role = get_object_or_404( - models.Role.objects.filter( - role_group__in=application_form.role_groups.all() - ), - pk=role_id, - ) - crew = get_object_or_404( - application_form.event.crews.filter(role_group=role.role_group), pk=crew_id - ) + # Delete existing assignment in the target role, if present + # `crew` is an override crew here. (RIGHT?? FIXME) + if assignment := models.CrewAssignment.objects.filter( + role=role, + crew=crew, + ).first(): + # Delete the assignment + assignment.delete() + + # Find the application corresponding to the existing assignment. + existing_application = ( + application_form.applications.filter( + user=assignment.user, roles__name=role.name + ) + .exclude(status=models.ApplicationStatus.WITHDRAWN) + .first() + ) + # There should be exactly one. + if existing_application: + existing_application.move_status_backwards_for_unassignment() + + # If we are removing one user from a static-crew assignment, add a blank assignment. + rgca = models.RoleGroupCrewAssignment.objects.filter( + role_group=role.role_group, + game=crew.get_context() # I think this is right? + ).first() + + if rgca and rgca.crew and not applications: + # rgca.crew is a static crew. + # Override with a blank in the override crew. + models.CrewAssignment.objects.create( + role=role, + crew=crew, + user=None + ) - if application_id: - applications = application_form.applications.filter( - id=application_id, - roles__name=role.name, - ).exclude(status=models.ApplicationStatus.WITHDRAWN) - if len(applications) == 0: - return HttpResponseBadRequest("invalid application_id") - else: - applications = [] + # Add a new assignment, if requested + if applications: + # If the newly-selected user is already assigned to + # an exclusive role, AND that assignment is within + # one of this form's Role Groups, clear that assignment. - # Delete existing assignment in the target role, if present - if assignment := models.CrewAssignment.objects.filter( - role=role, - crew=crew, - ).first(): - # Delete the assignment - assignment.delete() + # We need to assess any existing assignments for this user. + # If there are nonconflicting assignments, we'll ignore them. + # If there are "swappable" assignments, we'll delete them. + # If there are "non-swappable" assignments, we'll return an error. - # Find the application corresponding to the existing assignment. - existing_application = ( - application_form.applications.filter( - user=assignment.user, roles__name=role.name - ) - .exclude(status=models.ApplicationStatus.WITHDRAWN) - .first() - ) - # There should be exactly one. - if existing_application and not existing_application.has_assignments(): - # Reset its status appropriately. - if ( - existing_application.status - == models.ApplicationStatus.ASSIGNMENT_PENDING - ): - if ( - application_form.application_kind - == models.ApplicationKind.CONFIRM_THEN_ASSIGN - ): - existing_application.status = models.ApplicationStatus.CONFIRMED - else: - existing_application.status = models.ApplicationStatus.APPLIED - elif ( - existing_application.status - == models.ApplicationStatus.INVITATION_PENDING - ): - existing_application.status = models.ApplicationStatus.APPLIED - existing_application.save() - - # Add a new assignment, if requested - if applications: - # If the newly-selected user is already assigned to - # an exclusive role, AND that assignment is within - # one of this form's Role Groups, clear that assignment. - - # FIXME: we're deleting exclusive assignments when - # adding nonexclusive ones. - # FIXME: we aren't distinguishing between this form assignments - # and other-form assignments. - # FIXME: what if the user is on a static crew? We cannot override - # a static crew assignment to None - # TODO: display current assignment on crew builder detail - # TODO: show users who are time-available but not role-available - if assignments := models.CrewAssignment.objects.filter( - user=applications[0].user, - role__nonexclusive=False, - role__role_group__in=application_form.role_groups.all(), - crew__event=application_form.event, - ): - assignments.delete() + # An assignment is nonconflicting if it and this assignment are in + # the same role group and one of them is nonexclusive. + # An assignment is swappable if it is in one of the other role groups + # managed by this form (including this role group) and is conflicting. - models.CrewAssignment.objects.create( - role=role, crew=crew, user=applications[0].user - ) + # TODO: display current assignment on crew builder detail + # TODO: show users who are time-available but not role-available - # Update the status of the application - if ( - applications[0].form.application_kind - == models.ApplicationKind.ASSIGN_ONLY - ): - # Note that this sends apps in ASSIGNED status backwards, - # so they'll get an update email. - applications[0].status = models.ApplicationStatus.ASSIGNMENT_PENDING + old_assignments = am.get_swappable_assignments(crew, crew.get_context(), role) + old_assignments.delete() + + # Finally, add the new assignment. + + models.CrewAssignment.objects.create( + role=role, crew=crew, user=applications[0].user + ) + applications[0].move_status_forwards_for_assignment() + + # Redirect the user to the base Crew Builder for this crew + context = crew.get_context() + if isinstance(context, models.Game): + fragment = context.id else: - # for CONFIRM_THEN_ASSIGN events, our status update depends on the current status as well. - match applications[0].status: - case models.ApplicationStatus.APPLIED: - applications[ - 0 - ].status = models.ApplicationStatus.INVITATION_PENDING - case models.ApplicationStatus.CONFIRMED: - applications[ - 0 - ].status = models.ApplicationStatus.ASSIGNMENT_PENDING - case _: - # All other cases do not update. - pass - - applications[0].save() - - # Redirect the user to the base Crew Builder for this crew - context = crew.get_context() - if isinstance(context, models.Game): - fragment = context.id - else: - fragment = crew.id + fragment = crew.id - return HttpResponseRedirect( - reverse("crew-builder", args=[league, event_slug, application_form_slug]) - + f"#ctx-{fragment}" - ) + return HttpResponseRedirect( + reverse( + "crew-builder", args=[league, event_slug, application_form_slug] + ) + + f"#ctx-{fragment}" + ) class ApplicationFormView(views.View): From 3374ae8d4ca27b373cf60fb9e23a77b7133dded9 Mon Sep 17 00:00:00 2001 From: David Reed Date: Fri, 28 Nov 2025 13:35:01 -0700 Subject: [PATCH 07/26] WIP --- stave/avail.py | 100 ++++++++---------- stave/templates/stave/contexts.py | 10 +- .../templates/stave/crew_builder_detail.html | 20 ++-- .../stave/partials/application_table_row.html | 10 +- .../templates/stave/partials/crew_editor.html | 2 + stave/views.py | 50 +++++---- 6 files changed, 92 insertions(+), 100 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 9c9510e..4e37d28 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -15,6 +15,12 @@ class ConflictKind(enum.Enum): NON_SWAPPABLE_CONFLICT = 2 SWAPPABLE_CONFLICT = 3 +@dataclass +class ApplicationEntry: + application: models.Application + user_game_count: int + availability_status: ConflictKind + @dataclass class UserAvailabilityEntry: crew: models.Crew @@ -31,6 +37,8 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo if self.crew.id == other.crew.id: if self.exclusive and other.exclusive: return ConflictKind.SWAPPABLE_CONFLICT + else: + return ConflictKind.NONE match self.crew.kind: case models.CrewKind.OVERRIDE_CREW: @@ -48,11 +56,6 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo return ConflictKind.NONE -@dataclass -class AvailabilitySet: - available: list[models.Application] - swappable: list[models.Application] - unavailable: list[models.Application] class ScheduleManager: event: models.Event @@ -304,7 +307,8 @@ def get_game_count_for_user(self, user: models.User) -> int: @functools.cache def game_counts_by_user(self) -> dict[UUID, int]: return { - a.user.id: self.get_game_count_for_user(a.user) for a in self.applications + a.user.id: self.get_game_count_for_user(a.user) + for a in self.applications } @property @@ -341,11 +345,20 @@ def user_static_crew_availability(self) -> dict[UUID, list[UserAvailabilityEntry return user_static_map + @property + @functools.cache + def user_game_counts(self) -> dict[UUID, int]: + return { + a.user.id: self.get_game_count_for_user(a.user) + for a in all_applications + } + + def get_application_counts( self, crew: models.Crew, game: models.Game | None, role: models.Role ) -> tuple[int, int]: return ( - len(self.get_available_applications(crew, game, role)), + len([a for a in self.get_application_entries(crew, game, role) if a.availability_status is ConflictKind.NONE]), len(self.get_all_applications(crew, game, role)), ) @@ -358,28 +371,42 @@ def get_all_applications( ) @functools.cache - def get_applications_by_avail(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> AvailabilitySet: + def get_application_entries(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> list[ApplicationEntry]: apps = self.get_all_applications(crew, game, role) avail = self._get_avail_data_for_crew_kind(crew.kind) - avail_set = AvailabilitySet() - for app in applications: + entries = [] + + entry = UserAvailabilityEntry( + crew, + game.start_time if game else None, + game.end_time if game else None, + not role.nonexclusive, + ) + + for app in apps: + game_count = self.get_game_count_for_user(app.user) overlaps = set( t.overlaps(entry, self.role_groups) for t in avail[app.user_id] ) if ConflictKind.NON_SWAPPABLE_CONFLICT in overlaps: - avail_set.unavailable.append(app) + entries.append( + ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.NON_SWAPPABLE_CONFLICT) + ) elif ConflictKind.SWAPPABLE_CONFLICT in overlaps: - avail_set.swappable.append(app) + entries.append( + ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.SWAPPABLE_CONFLICT) + ) else: - avail_set.available.append(app) - - return avail_set + entries.append( + ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.NONE) + ) + return entries @functools.cache def get_swappable_assignments( - self, crew: models.Crew, game: models.Game | None, role: models.Role - ) -> list[models.CrewAssignment]: + self, user: models.User, crew: models.Crew, game: models.Game | None, role: models.Role + ) -> list[UserAvailabilityEntry]: avail = self._get_avail_data_for_crew_kind(crew.kind) entry = UserAvailabilityEntry( @@ -389,16 +416,9 @@ def get_swappable_assignments( not role.nonexclusive, ) - # TODO: go from apps to crew assignments - return list( - filter( - lambda app: all( - t.overlaps(entry, self.role_groups) in [ConflictKind.SWAPPABLE_CONFLICT, ConflictKind.NONE] - for t in self._get_avail_data_for_crew_kind(crew.kind)[app.user_id] - ), - applications, - ) - ) + return [t for t in self._get_avail_data_for_crew_kind(crew.kind)[user.id] + if t.overlaps(entry, self.role_groups) is ConflictKind.SWAPPABLE_CONFLICT + ] # Filter methods MUST NOT hit the database - use only cached data @@ -444,29 +464,3 @@ def _filter_for_basic_availability( ] return applications - - def _filter_for_already_assigned_users( - self, - applications: Iterable[models.Application], - crew: models.Crew, - game: models.Game | None, - role: models.Role, - allowed_conflict_kinds: set[ConflictKind] - ) -> Iterable[models.Application]: - entry = UserAvailabilityEntry( - crew, - game.start_time if game else None, - game.end_time if game else None, - not role.nonexclusive, - ) - avail = self._get_avail_data_for_crew_kind(crew.kind) - for app in applications: - overlaps = set( - t.overlaps(entry, self.role_groups) for t in avail[app.user_id] - ) - filter( - lambda app: all( - ), - applications, - ) - ) diff --git a/stave/templates/stave/contexts.py b/stave/templates/stave/contexts.py index 6c24283..242afde 100644 --- a/stave/templates/stave/contexts.py +++ b/stave/templates/stave/contexts.py @@ -6,8 +6,7 @@ from django.db.models import QuerySet from django.http import HttpRequest -from stave import forms, models - +from stave import forms, models, avail def to_dict(obj) -> dict: return {field.name: getattr(obj, field.name) for field in fields(obj)} @@ -31,8 +30,7 @@ class ApplicationTableInputs: @dataclass class ApplicationTableRowInputs: form: models.ApplicationForm - application: models.Application - game_count: int + entry: avail.ApplicationEntry @dataclass @@ -52,9 +50,7 @@ class CrewBuilderDetailInputs: form: models.ApplicationForm role: models.Role game: models.Game | None - applications: list[models.Application] - unavail_applications: list[models.Application] - game_counts: dict[UUID, int] + applications: list[avail.ApplicationEntry] @dataclass diff --git a/stave/templates/stave/crew_builder_detail.html b/stave/templates/stave/crew_builder_detail.html index b42c470..ee244ba 100644 --- a/stave/templates/stave/crew_builder_detail.html +++ b/stave/templates/stave/crew_builder_detail.html @@ -14,20 +14,18 @@

{% include 'stave/partials/application_table_header.html' with form=form only %} - {% for application in applications %} + {% for entry in applications %} - {% with game_count=game_counts|get:application.user_id %} - {% include 'stave/partials/application_table_row.html' with form=form application=application game_count=game_count only %} - {% endwith %} + {% include 'stave/partials/application_table_row.html' with form=form entry=entry only %} {% empty %}
{% csrf_token %} - + - View + View
@@ -44,20 +42,18 @@

{% include 'stave/partials/application_table_header.html' with form=form only %} - {% for application in unavail_applications %} + {% for entry in unavail_applications %} - {% with game_count=game_counts|get:application.user_id %} - {% include 'stave/partials/application_table_row.html' with form=form application=application game_count=game_count only %} - {% endwith %} + {% include 'stave/partials/application_table_row.html' with form=form entry=entry only %} {% endfor %} diff --git a/stave/templates/stave/partials/application_table_row.html b/stave/templates/stave/partials/application_table_row.html index 56118b3..075b4e1 100644 --- a/stave/templates/stave/partials/application_table_row.html +++ b/stave/templates/stave/partials/application_table_row.html @@ -1,7 +1,7 @@ {% load stave_tags %} {% inputs 'ApplicationTableRowInputs' %} - - + + {# Profile fields #} {% for field in application.form.requires_profile_fields %} {% endfor %} {# Roles #} -{% for role_group in application.form.role_groups.all %} +{% for role_group in entry.application.form.role_groups.all %} {% for entry in applications %} + {% if entry.availability_status != ConflictKind.NON_SWAPPABLE_CONFLICT %} {% include 'stave/partials/application_table_row.html' with form=form entry=entry only %} + {% endif %} {% empty %} {% with assignment=crew_assignments|get:role.id %} - {% with game_count=game_counts|get:application.user_id %} - {% include 'stave/partials/application_table_row.html' with form=form application=application game_count=game_count only %} - {% endwith %} + {% include 'stave/partials/application_table_row.html' with form=form entry=entry only %} {% endfor %} {% endpartialdef %} diff --git a/stave/templates/stave/partials/crew_editor.html b/stave/templates/stave/partials/crew_editor.html index 703c0a7..ac396d1 100644 --- a/stave/templates/stave/partials/crew_editor.html +++ b/stave/templates/stave/partials/crew_editor.html @@ -47,7 +47,7 @@ {% endif %} - + {% if assignment and assignment.user %} {% endif %} diff --git a/stave/views.py b/stave/views.py index 5048570..7b8ebe2 100644 --- a/stave/views.py +++ b/stave/views.py @@ -39,7 +39,7 @@ from stave.templates.stave import contexts from . import forms, models, settings -from .avail import AvailabilityManager, ScheduleManager, ConflictKind +from .avail import AvailabilityManager, ScheduleManager, ConflictKind, ApplicationEntry, UserNotAvailableException if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -1614,15 +1614,38 @@ def get_context(self) -> contexts.FormApplicationsInputs: am = AvailabilityManager.with_application_form(form) return contexts.FormApplicationsInputs( form=form, - applications_action=am.get_applications_in_statuses(models.OPEN_STATUSES), - applications_inprogress=am.get_applications_in_statuses( - models.IN_PROGRESS_STATUSES - ), - applications_staffed=am.get_applications_in_statuses( - models.STAFFED_STATUSES - ), - applications_closed=am.get_applications_in_statuses(models.CLOSED_STATUSES), - game_counts=am.game_counts_by_user, + applications_action=[ + ApplicationEntry( + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE + ) + for a in am.get_applications_in_statuses(models.OPEN_STATUSES) + ], + applications_inprogress=[ + ApplicationEntry( + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE + ) + for a in am.get_applications_in_statuses(models.IN_PROGRESS_STATUSES) + ], + applications_staffed=[ + ApplicationEntry( + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE + ) + for a in am.get_applications_in_statuses(models.STAFFED_STATUSES) + ], + applications_closed=[ + ApplicationEntry( + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE + ) + for a in am.get_applications_in_statuses(models.CLOSED_STATUSES) + ], ApplicationStatus=models.ApplicationStatus, ) @@ -1761,7 +1784,6 @@ def post( crew_id: UUID | None = None, ) -> HttpResponse: with transaction.atomic(): - # FIXME: check availability for all crew members. game: models.Game = get_object_or_404( models.Game.objects.manageable(request.user), pk=game_id, @@ -1779,6 +1801,8 @@ def post( else: crew = None + # FIXME: if overwriting existing crew, remove null overrides. + # FIXME: check availability for all crew members. Add null overrides if needed. rgca.crew = crew rgca.save() @@ -2094,13 +2118,14 @@ def post( role_id: UUID, ) -> HttpResponse: with transaction.atomic(): - application_id = request.POST.get("application_id") + user_id = request.POST.get("user_id") application_form: models.ApplicationForm = get_object_or_404( models.ApplicationForm.objects.manageable(request.user), slug=application_form_slug, event__slug=event_slug, event__league__slug=league, ) + # TODO: we can reduce DB round-trips by getting these from am. role = get_object_or_404( models.Role.objects.filter( role_group__in=application_form.role_groups.all() @@ -2113,107 +2138,19 @@ def post( ) am = AvailabilityManager.with_application_form(application_form) - if application_id: - applications = application_form.applications.filter( - id=application_id, - roles__name=role.name, - ).exclude(status=models.ApplicationStatus.WITHDRAWN) - if len(applications) != 1: - return HttpResponseBadRequest("invalid application_id") - else: - applications = [] - - # Delete existing assignment in the target role, if present - # `crew` is an override crew here. - if assignment := models.CrewAssignment.objects.filter( - role=role, - crew=crew, - ).first(): - # Delete the assignment - assignment.delete() - - # Find the application corresponding to the existing assignment. - existing_application = ( - application_form.applications.filter( - user=assignment.user, roles__name=role.name - ) - .exclude(status=models.ApplicationStatus.WITHDRAWN) - .first() + user = None + if user_id: + user = get_object_or_404( + models.User, + pk=user_id ) - # There should be one or zero. - if existing_application: - existing_application.move_status_backwards_for_unassignment() - - # FIXME: if we have a static crew assigned and the underlying user - # is not available, add a None override. - - # If we are removing one user from a static-crew assignment, add a blank assignment. - rgca = models.RoleGroupCrewAssignment.objects.filter( - role_group=role.role_group, game=crew.get_context() - ).first() - - if rgca and rgca.crew and not assignment and not applications: - # rgca.crew is a static crew. - # Override with a blank in the override crew. - models.CrewAssignment.objects.create(role=role, crew=crew, user=None) - if applications: - # If the newly-selected user is already assigned to - # an exclusive role, AND that assignment is within - # one of this form's Role Groups, clear that assignment. - - # TODO: display current assignment on crew builder detail - # TODO: show users who are time-available but not role-available - - old_availability_entries = am.get_swappable_assignments( - applications[0].user, crew, crew.get_context(), role - ) - for avail_entry in old_availability_entries: - # This UserAvailabilityEntry might come from a direct assignment - # or from a static crew assignment; there are different ways - # to override those. - match avail_entry.crew.kind: - case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: - # Add an overriding CrewAssignment to blank for each assignment - # of this user in the crew, unless we're assigning to the same - # game, in which case we keep nonexclusive assignments. - # FIXME: ensure that we do not allow this CrewAssignment to be deleted - keep_nonexclusive = ( - # Swap within a crew - avail_entry.crew == crew - # Swap from a static crew to an override crew in the same context - or avail_entry.crew.get_context() == crew.get_context() - ) - cas = models.CrewAssignment.objects.filter( - crew=avail_entry.crew, user=applications[0].user - ) - if keep_nonexclusive: - cas = cas.exclude(role__nonexclusive=True) - - for ca in cas: - models.CrewAssignment.objects.create( - role=ca.role, crew=crew, user=None - ) - case models.CrewKind.OVERRIDE_CREW: - # Just query for and delete the relevant CrewAssignments - # If we're reassigning within the same crew, - # just remove exclusive roles; otherwise, all roles. - cas = models.CrewAssignment.objects.filter( - user=applications[0].user, crew=avail_entry.crew - ) - if crew == avail_entry.crew: - cas = cas.exclude(role__nonexclusive=True) - - cas.delete() - - # Finally, add the new assignment. - - models.CrewAssignment.objects.create( - role=role, crew=crew, user=applications[0].user - ) - applications[0].move_status_forwards_for_assignment() + try: + am.set_assignment(role, crew, user) + except UserNotAvailableException: + messages.info(request, _("The selected user is not available for the chosen slot.")) - # Redirect the user to the base Crew Builder for this crew + # Redirect the user to the Crew Builder for this crew context = crew.get_context() if isinstance(context, models.Game): fragment = context.id diff --git a/tests/test_avail.py b/tests/test_avail.py index 45ae751..75d417d 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -52,6 +52,33 @@ def test_user_availability_entry__full_overlap_same_crew_exclusive(existing_entr == ConflictKind.SWAPPABLE_CONFLICT ) +def test_user_availability_entry__abutting_left_same_crew_exclusive(existing_entry): + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.end_time, + end_time=existing_entry.end_time + timedelta(hours=2), + exclusive=True, + ), + set(), + ) + == ConflictKind.NONE + ) + +def test_user_availability_entry__abutting_right_same_crew_exclusive(existing_entry): + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time - timedelta(hours=2), + end_time=existing_entry.start_time, + exclusive=True, + ), + set(), + ) + == ConflictKind.NONE + ) def test_user_availability_entry__full_overlap_same_crew_nonexclusive(existing_entry): assert ( From 7844befd0464e2d4f9ab481cf6c0e7ba367fb7e8 Mon Sep 17 00:00:00 2001 From: David Reed Date: Fri, 19 Dec 2025 11:30:45 -0700 Subject: [PATCH 18/26] Tests WIP --- stave/avail.py | 138 ++++++++++++++++++++++---------------------- tests/factories.py | 1 + tests/test_avail.py | 23 +++++--- 3 files changed, 86 insertions(+), 76 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index d96fe21..6e9e934 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -6,6 +6,7 @@ from uuid import UUID import enum +from django.db import transaction from django.db.models import Prefetch, QuerySet from . import models @@ -62,7 +63,6 @@ def overlaps( return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NONE - elif has_times: # This is a comparison between two override crews, which always # have defined start and end times, or between two assigned static crews @@ -552,76 +552,76 @@ def set_assignment( crew: models.Crew, user: models.User | None, ): - # Regardless of the "to" side of this assignment, - # we need to remove any existing assignment. - existing_assignment = self.get_assignment(role, crew) - if existing_assignment: - application = self.get_application_for_assignment(existing_assignment) - existing_assignment.delete() - if application: - application.move_status_backwards_for_unassignment() - - # If necessary, swap the new user out of their existing assignments. - if user: - application = self.get_application_for_user(user) - if application: - application.move_status_forwards_for_assignment() - else: - raise UserNotAvailableException("There is no application for user") + with transaction.atomic(): + # Regardless of the "to" side of this assignment, + # we need to remove any existing assignment. + existing_assignment = self.get_assignment(role, crew) + if existing_assignment: + application = self.get_application_for_assignment(existing_assignment) + existing_assignment.delete() + if application: + application.move_status_backwards_for_unassignment() + + # If necessary, swap the new user out of their existing assignments. + if user: + application = self.get_application_for_user(user) + if application: + application.move_status_forwards_for_assignment() + else: + raise UserNotAvailableException("There is no application for user") - old_availability_entries = self.get_swappable_assignments( - user, crew, crew.get_context(), role - ) - for avail_entry in old_availability_entries: - # This UserAvailabilityEntry might come from a direct assignment - # or from a static crew assignment; there are different ways - # to replace those. - # FIXME: could we get back a static crew entry when we are blanking - # an override? - match avail_entry.crew.kind: - case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: - keep_nonexclusive = ( - # Swap within a crew - avail_entry.crew == crew - # Swap from a static crew to an override crew in the same context - or avail_entry.crew.get_context() == crew.get_context() + old_availability_entries = self.get_swappable_assignments( + user, crew, crew.get_context(), role + ) + for avail_entry in old_availability_entries: + # This UserAvailabilityEntry might come from a direct assignment + # or from a static crew assignment; there are different ways + # to replace those. + # FIXME: could we get back a static crew entry when we are blanking + # an override? + match avail_entry.crew.kind: + case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: + keep_nonexclusive = ( + # Swap within a crew + avail_entry.crew == crew + # Swap from a static crew to an override crew in the same context + or avail_entry.crew.get_context() == crew.get_context() + ) + case models.CrewKind.OVERRIDE_CREW: + # If we're reassigning within the same crew, + # just remove exclusive roles; otherwise, all roles. + keep_nonexclusive = avail_entry.crew == crew + + # This is a set, not a list, because when we're overriding a static crew + # assignment, we'll get back a CrewAssignment for every game that the + # static crew is assigned to from game_crew_assignments + cas = { + ca + for (_, ca) in self.game_crew_assignments + if ( + ca.crew == avail_entry.crew + and ca.user == user ) - case models.CrewKind.OVERRIDE_CREW: - # If we're reassigning within the same crew, - # just remove exclusive roles; otherwise, all roles. - keep_nonexclusive = avail_entry.crew == crew - - # This is a set, not a list, because when we're overriding a static crew - # assignment, we'll get back a CrewAssignment for every game that the - # static crew is assigned to from game_crew_assignments - cas = { - ca - for (_, ca) in self.game_crew_assignments - if ( - ca.crew == avail_entry.crew - and ca.user == user - ) - } - if keep_nonexclusive: - cas = {ca for ca in cas if not ca.role.nonexclusive} - - # Add an overriding CrewAssignment to blank for each relevant - # assignment. This ensures that, if we are removing an override - # crew assignment, any underlying static crew member is not - # silently re-staffed into this role. - # If this is an override crew, also delete the original CrewAssignment. - # - # Note that `crew` is not necessarily `avail_entry.crew`. - # If the latter is a static crew, the former is an override crew. - breakpoint() - for ca in cas: - if avail_entry.crew.kind == models.CrewKind.OVERRIDE_CREW: - ca.delete() - models.CrewAssignment.objects.create( - role=ca.role, crew=crew, user=None - ) - # FIXME: we could then detect if we're assigning user X - # when a static crew would assign X, and do nothing. + } + if keep_nonexclusive: + cas = {ca for ca in cas if not ca.role.nonexclusive} + + # Add an overriding CrewAssignment to blank for each relevant + # assignment. This ensures that, if we are removing an override + # crew assignment, any underlying static crew member is not + # silently re-staffed into this role. + # If this is an override crew, also delete the original CrewAssignment. + # + # Note that `crew` is not necessarily `avail_entry.crew`. + # If the latter is a static crew, the former is an override crew. + for ca in cas: + if avail_entry.crew.kind == models.CrewKind.OVERRIDE_CREW: + ca.delete() + models.CrewAssignment.objects.create( + role=ca.role, crew=crew, user=None + ) + # FIXME: we could then detect if we're assigning user X + # when a static crew would assign X, and do nothing. # Finally, add the new entry. # Note that this might be a null override on top of a static crew, diff --git a/tests/factories.py b/tests/factories.py index 404445e..b1aaf19 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -186,6 +186,7 @@ class Meta: ) # TODO: applicationformtemplates + # TODO: role groups questions = factory.RelatedFactoryList( "tests.factories.QuestionFactory", factory_related_name="application_form", diff --git a/tests/test_avail.py b/tests/test_avail.py index 75d417d..1e43d9a 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -1,6 +1,6 @@ from zeal import zeal_ignore -from tests.factories import ApplicationFactory, CrewFactory +from tests.factories import ApplicationFactory, CrewFactory, ApplicationFormFactory from datetime import datetime, timedelta, timezone from stave.avail import AvailabilityManager, UserAvailabilityEntry, ConflictKind from pytest import fixture @@ -306,11 +306,11 @@ def test_availability_manager__applications(tournament): # TODO: test that prefetches cache # - def test_availability_manager__applications_by_status(db): +def test_availability_manager__applications_by_status(db): application_form = ApplicationFormFactory() for status in models.ApplicationStatus: for _ in range(3): - ApplicationFactory(application_form=application_form, status=status) + ApplicationFactory(form=application_form, status=status) am = AvailabilityManager.with_application_form(application_form) by_status = am.applications_by_status() @@ -318,11 +318,11 @@ def test_availability_manager__applications_by_status(db): assert keys(by_status) == list(models.ApplicationStatus) assert all(len(v) == 3 for v in by_status.values()) - def test_availability_manager__get_applications_in_statuses(db): +def test_availability_manager__get_applications_in_statuses(db): application_form = ApplicationFormFactory() for status in models.ApplicationStatus: for i in range(3): - ApplicationFactory(user__preferred_name="CBA"[i], application_form=application_form, status=status) + ApplicationFactory(user__preferred_name="CBA"[i], form=application_form, status=status) am = AvailabilityManager.with_application_form(application_form) in_statuses = am.get_applications_in_statuses((models.ApplicationStatus.APPLIED, models.ApplicationStatus.ASSIGNED)) @@ -330,7 +330,7 @@ def test_availability_manager__get_applications_in_statuses(db): assert len(in_statuses) == 6 assert in_statuses == sorted(in_statuses, key=lambda a: a.user.preferred_name) - def test_availability_manager__static_crews(db): +def test_availability_manager__static_crews(db): application_form = ApplicationFormFactory() crew = CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) @@ -339,7 +339,7 @@ def test_availability_manager__static_crews(db): assert am.static_crews == [crew] - def test_availability_manager__event_crews(db): +def test_availability_manager__event_crews(db): application_form = ApplicationFormFactory() crew = CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) @@ -347,3 +347,12 @@ def test_availability_manager__event_crews(db): am = AvailabilityManager.with_application_form(application_form) assert am.event_crews == [crew] + + +def test_set_assignment__override_crew_ex_nihilo(db): + application_form = ApplicationFormFactory() + application_form.role_groups.set(application_form.event.league.role_groups.all()) + + am = AvailabilityManager.with_application_form(application_form) + + am.set_assignment(role, crew, user) From 11423855d325466a71650232ec825f0fbb371683 Mon Sep 17 00:00:00 2001 From: David Reed Date: Mon, 2 Feb 2026 19:56:43 -0700 Subject: [PATCH 19/26] Merge migrations --- stave/migrations/0059_merge_20260203_0256.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 stave/migrations/0059_merge_20260203_0256.py diff --git a/stave/migrations/0059_merge_20260203_0256.py b/stave/migrations/0059_merge_20260203_0256.py new file mode 100644 index 0000000..8880613 --- /dev/null +++ b/stave/migrations/0059_merge_20260203_0256.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.3 on 2026-02-03 02:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stave', '0056_alter_crewassignment_user'), + ('stave', '0058_merge_20260125_2347'), + ] + + operations = [ + ] From e43b2de68675347d2b23a30fdffc1cdf0288b588 Mon Sep 17 00:00:00 2001 From: David Reed Date: Mon, 2 Feb 2026 19:57:15 -0700 Subject: [PATCH 20/26] Ruff --- stave/avail.py | 78 +++++++++------- stave/migrations/0059_merge_20260203_0256.py | 8 +- stave/views.py | 43 +++++---- tests/test_avail.py | 97 +++++++++++++------- 4 files changed, 133 insertions(+), 93 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 6e9e934..a462550 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -2,7 +2,7 @@ from collections import defaultdict from dataclasses import dataclass from datetime import datetime -from typing import Iterable,Generator, Tuple +from typing import Iterable, Generator, Tuple from uuid import UUID import enum @@ -20,9 +20,11 @@ class ConflictKind(enum.Enum): ConflictKind.do_not_call_in_templates = True + class UserNotAvailableException(Exception): pass + @dataclass class ApplicationEntry: application: models.Application @@ -64,32 +66,31 @@ def overlaps( else: return ConflictKind.NONE elif has_times: - # This is a comparison between two override crews, which always - # have defined start and end times, or between two assigned static crews - # or an assigned static crew and an override crew, which will - # also have times. - time_overlap = ( - self.start_time < other.end_time - and self.end_time > other.start_time - ) - - if time_overlap: - if ( - other.crew.role_group in swappable_role_groups - or other.crew.role_group == self.crew.role_group - ): - return ConflictKind.SWAPPABLE_CONFLICT - else: - return ConflictKind.NON_SWAPPABLE_CONFLICT + # This is a comparison between two override crews, which always + # have defined start and end times, or between two assigned static crews + # or an assigned static crew and an override crew, which will + # also have times. + time_overlap = ( + self.start_time < other.end_time and self.end_time > other.start_time + ) - elif self.crew.kind == other.crew.kind: - # This comparison is between static crews _without_ assignments - # or between event crews. No times involved. - if other.crew.role_group in swappable_role_groups: + if time_overlap: + if ( + other.crew.role_group in swappable_role_groups + or other.crew.role_group == self.crew.role_group + ): return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NON_SWAPPABLE_CONFLICT + elif self.crew.kind == other.crew.kind: + # This comparison is between static crews _without_ assignments + # or between event crews. No times involved. + if other.crew.role_group in swappable_role_groups: + return ConflictKind.SWAPPABLE_CONFLICT + else: + return ConflictKind.NON_SWAPPABLE_CONFLICT + return ConflictKind.NONE @@ -303,7 +304,9 @@ def event_crews(self) -> list[models.Crew]: ] @property - def game_crew_assignments(self) -> Generator[Tuple[models.Game, models.CrewAssignment]]: + def game_crew_assignments( + self, + ) -> Generator[Tuple[models.Game, models.CrewAssignment]]: for game in self.application_form.event.games.all(): for rgca in game.role_group_crew_assignments.all(): for ca in rgca.effective_crew_by_role_id().values(): @@ -314,7 +317,7 @@ def game_crew_assignments(self) -> Generator[Tuple[models.Game, models.CrewAssig def user_availability(self) -> dict[UUID, list[UserAvailabilityEntry]]: user_assigned_times_map = defaultdict(list) - for (game, assignment) in self.game_crew_assignments: + for game, assignment in self.game_crew_assignments: if assignment.user_id: user_assigned_times_map[assignment.user_id].append( UserAvailabilityEntry( @@ -523,16 +526,24 @@ def _filter_for_basic_availability( return applications def get_application_by_id(self, id: UUID) -> models.Application | None: - for application in self.applications: - if application.id== id and application.status != models.ApplicationStatus.WITHDRAWN: - return application + for application in self.applications: + if ( + application.id == id + and application.status != models.ApplicationStatus.WITHDRAWN + ): + return application def get_application_for_user(self, user: models.User) -> models.Application | None: - for application in self.applications: - if application.user == user and application.status != models.ApplicationStatus.WITHDRAWN: - return application + for application in self.applications: + if ( + application.user == user + and application.status != models.ApplicationStatus.WITHDRAWN + ): + return application - def get_application_for_assignment(self, assignment: models.CrewAssignment) -> models.Application | None: + def get_application_for_assignment( + self, assignment: models.CrewAssignment + ) -> models.Application | None: # There should be exactly one or zero non-withdrawn applications for this user. if assignment.user: return self.get_application_for_user(assignment.user) @@ -598,10 +609,7 @@ def set_assignment( cas = { ca for (_, ca) in self.game_crew_assignments - if ( - ca.crew == avail_entry.crew - and ca.user == user - ) + if (ca.crew == avail_entry.crew and ca.user == user) } if keep_nonexclusive: cas = {ca for ca in cas if not ca.role.nonexclusive} diff --git a/stave/migrations/0059_merge_20260203_0256.py b/stave/migrations/0059_merge_20260203_0256.py index 8880613..355c04e 100644 --- a/stave/migrations/0059_merge_20260203_0256.py +++ b/stave/migrations/0059_merge_20260203_0256.py @@ -4,11 +4,9 @@ class Migration(migrations.Migration): - dependencies = [ - ('stave', '0056_alter_crewassignment_user'), - ('stave', '0058_merge_20260125_2347'), + ("stave", "0056_alter_crewassignment_user"), + ("stave", "0058_merge_20260125_2347"), ] - operations = [ - ] + operations = [] diff --git a/stave/views.py b/stave/views.py index 7b8ebe2..ee1cb30 100644 --- a/stave/views.py +++ b/stave/views.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -import logging from collections import defaultdict import csv from dataclasses import is_dataclass @@ -39,7 +38,13 @@ from stave.templates.stave import contexts from . import forms, models, settings -from .avail import AvailabilityManager, ScheduleManager, ConflictKind, ApplicationEntry, UserNotAvailableException +from .avail import ( + AvailabilityManager, + ScheduleManager, + ConflictKind, + ApplicationEntry, + UserNotAvailableException, +) if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -1616,33 +1621,33 @@ def get_context(self) -> contexts.FormApplicationsInputs: form=form, applications_action=[ ApplicationEntry( - application=a, - user_game_count=am.get_game_count_for_user(a.user), - availability_status=ConflictKind.NONE + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE, ) for a in am.get_applications_in_statuses(models.OPEN_STATUSES) ], applications_inprogress=[ ApplicationEntry( - application=a, - user_game_count=am.get_game_count_for_user(a.user), - availability_status=ConflictKind.NONE + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE, ) for a in am.get_applications_in_statuses(models.IN_PROGRESS_STATUSES) ], applications_staffed=[ ApplicationEntry( - application=a, - user_game_count=am.get_game_count_for_user(a.user), - availability_status=ConflictKind.NONE + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE, ) for a in am.get_applications_in_statuses(models.STAFFED_STATUSES) ], applications_closed=[ ApplicationEntry( - application=a, - user_game_count=am.get_game_count_for_user(a.user), - availability_status=ConflictKind.NONE + application=a, + user_game_count=am.get_game_count_for_user(a.user), + availability_status=ConflictKind.NONE, ) for a in am.get_applications_in_statuses(models.CLOSED_STATUSES) ], @@ -2140,15 +2145,15 @@ def post( user = None if user_id: - user = get_object_or_404( - models.User, - pk=user_id - ) + user = get_object_or_404(models.User, pk=user_id) try: am.set_assignment(role, crew, user) except UserNotAvailableException: - messages.info(request, _("The selected user is not available for the chosen slot.")) + messages.info( + request, + _("The selected user is not available for the chosen slot."), + ) # Redirect the user to the Crew Builder for this crew context = crew.get_context() diff --git a/tests/test_avail.py b/tests/test_avail.py index 1e43d9a..efb806a 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -28,6 +28,7 @@ def existing_static_crew_entry(db): exclusive=True, ) + @fixture def existing_event_crew_entry(db): return UserAvailabilityEntry( @@ -52,6 +53,7 @@ def test_user_availability_entry__full_overlap_same_crew_exclusive(existing_entr == ConflictKind.SWAPPABLE_CONFLICT ) + def test_user_availability_entry__abutting_left_same_crew_exclusive(existing_entry): assert ( existing_entry.overlaps( @@ -66,6 +68,7 @@ def test_user_availability_entry__abutting_left_same_crew_exclusive(existing_ent == ConflictKind.NONE ) + def test_user_availability_entry__abutting_right_same_crew_exclusive(existing_entry): assert ( existing_entry.overlaps( @@ -80,6 +83,7 @@ def test_user_availability_entry__abutting_right_same_crew_exclusive(existing_en == ConflictKind.NONE ) + def test_user_availability_entry__full_overlap_same_crew_nonexclusive(existing_entry): assert ( existing_entry.overlaps( @@ -141,7 +145,9 @@ def test_user_availability_entry__partial_overlap(existing_entry): ) -def test_user_availability_entry__overlap_same_static_crew_exclusive(existing_static_crew_entry): +def test_user_availability_entry__overlap_same_static_crew_exclusive( + existing_static_crew_entry, +): assert ( existing_static_crew_entry.overlaps( UserAvailabilityEntry( @@ -156,7 +162,9 @@ def test_user_availability_entry__overlap_same_static_crew_exclusive(existing_st ) -def test_user_availability_entry__overlap_same_static_crew_nonexclusive(existing_static_crew_entry): +def test_user_availability_entry__overlap_same_static_crew_nonexclusive( + existing_static_crew_entry, +): assert ( existing_static_crew_entry.overlaps( UserAvailabilityEntry( @@ -208,7 +216,10 @@ def test_user_availability_entry__overlap_other_static_crew_swappable( def test_user_availability_entry__override_of_static_crew(): ... -def test_user_availability_entry__overlap_same_event_crew_exclusive(existing_event_crew_entry): + +def test_user_availability_entry__overlap_same_event_crew_exclusive( + existing_event_crew_entry, +): assert ( existing_event_crew_entry.overlaps( UserAvailabilityEntry( @@ -222,7 +233,10 @@ def test_user_availability_entry__overlap_same_event_crew_exclusive(existing_eve == ConflictKind.SWAPPABLE_CONFLICT ) -def test_user_availability_entry__overlap_same_event_crew_nonexclusive(existing_event_crew_entry): + +def test_user_availability_entry__overlap_same_event_crew_nonexclusive( + existing_event_crew_entry, +): assert ( existing_event_crew_entry.overlaps( UserAvailabilityEntry( @@ -236,7 +250,10 @@ def test_user_availability_entry__overlap_same_event_crew_nonexclusive(existing_ == ConflictKind.NONE ) -def test_user_availability_entry__overlap_other_event_crew_non_swappable(existing_event_crew_entry): + +def test_user_availability_entry__overlap_other_event_crew_non_swappable( + existing_event_crew_entry, +): assert ( existing_event_crew_entry.overlaps( UserAvailabilityEntry( @@ -250,7 +267,10 @@ def test_user_availability_entry__overlap_other_event_crew_non_swappable(existin == ConflictKind.NON_SWAPPABLE_CONFLICT ) -def test_user_availability_entry__overlap_other_event_crew_swappable(existing_event_crew_entry): + +def test_user_availability_entry__overlap_other_event_crew_swappable( + existing_event_crew_entry, +): other_crew = CrewFactory(kind=models.CrewKind.EVENT_CREW) assert ( existing_event_crew_entry.overlaps( @@ -265,6 +285,7 @@ def test_user_availability_entry__overlap_other_event_crew_swappable(existing_ev == ConflictKind.SWAPPABLE_CONFLICT ) + def test_user_availability_entry__non_meaningful(existing_event_crew_entry): other_crew = CrewFactory(kind=models.CrewKind.GAME_CREW) assert ( @@ -306,47 +327,55 @@ def test_availability_manager__applications(tournament): # TODO: test that prefetches cache # + def test_availability_manager__applications_by_status(db): - application_form = ApplicationFormFactory() - for status in models.ApplicationStatus: - for _ in range(3): - ApplicationFactory(form=application_form, status=status) + application_form = ApplicationFormFactory() + for status in models.ApplicationStatus: + for _ in range(3): + ApplicationFactory(form=application_form, status=status) - am = AvailabilityManager.with_application_form(application_form) - by_status = am.applications_by_status() + am = AvailabilityManager.with_application_form(application_form) + by_status = am.applications_by_status() + + assert keys(by_status) == list(models.ApplicationStatus) + assert all(len(v) == 3 for v in by_status.values()) - assert keys(by_status) == list(models.ApplicationStatus) - assert all(len(v) == 3 for v in by_status.values()) def test_availability_manager__get_applications_in_statuses(db): - application_form = ApplicationFormFactory() - for status in models.ApplicationStatus: - for i in range(3): - ApplicationFactory(user__preferred_name="CBA"[i], form=application_form, status=status) + application_form = ApplicationFormFactory() + for status in models.ApplicationStatus: + for i in range(3): + ApplicationFactory( + user__preferred_name="CBA"[i], form=application_form, status=status + ) - am = AvailabilityManager.with_application_form(application_form) - in_statuses = am.get_applications_in_statuses((models.ApplicationStatus.APPLIED, models.ApplicationStatus.ASSIGNED)) + am = AvailabilityManager.with_application_form(application_form) + in_statuses = am.get_applications_in_statuses( + (models.ApplicationStatus.APPLIED, models.ApplicationStatus.ASSIGNED) + ) + + assert len(in_statuses) == 6 + assert in_statuses == sorted(in_statuses, key=lambda a: a.user.preferred_name) - assert len(in_statuses) == 6 - assert in_statuses == sorted(in_statuses, key=lambda a: a.user.preferred_name) def test_availability_manager__static_crews(db): - application_form = ApplicationFormFactory() - crew = CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) - CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) - CrewFactory(kind=models.CrewKind.GAME_CREW) - am = AvailabilityManager.with_application_form(application_form) + application_form = ApplicationFormFactory() + crew = CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) + CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) + CrewFactory(kind=models.CrewKind.GAME_CREW) + am = AvailabilityManager.with_application_form(application_form) + + assert am.static_crews == [crew] - assert am.static_crews == [crew] def test_availability_manager__event_crews(db): - application_form = ApplicationFormFactory() - crew = CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) - CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) - CrewFactory(kind=models.CrewKind.EVENT_CREW) - am = AvailabilityManager.with_application_form(application_form) + application_form = ApplicationFormFactory() + crew = CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) + CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) + CrewFactory(kind=models.CrewKind.EVENT_CREW) + am = AvailabilityManager.with_application_form(application_form) - assert am.event_crews == [crew] + assert am.event_crews == [crew] def test_set_assignment__override_crew_ex_nihilo(db): From f60d626920537ecb5f84455929922594e2ead517 Mon Sep 17 00:00:00 2001 From: David Reed Date: Tue, 3 Feb 2026 19:00:54 -0700 Subject: [PATCH 21/26] Fix some tests --- tests/test_avail.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_avail.py b/tests/test_avail.py index efb806a..1e29cd3 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -335,9 +335,11 @@ def test_availability_manager__applications_by_status(db): ApplicationFactory(form=application_form, status=status) am = AvailabilityManager.with_application_form(application_form) - by_status = am.applications_by_status() + by_status = am.applications_by_status - assert keys(by_status) == list(models.ApplicationStatus) + # AvailabilityManager doesn't pull apps in statuses + # that have no effect on availability, like Withdrawn + assert set(by_status.keys()).issubset(set(v.value for v in models.ApplicationStatus)) assert all(len(v) == 3 for v in by_status.values()) @@ -360,8 +362,12 @@ def test_availability_manager__get_applications_in_statuses(db): def test_availability_manager__static_crews(db): application_form = ApplicationFormFactory() - crew = CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) - CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) + application_form.role_groups.set(application_form.event.league.role_groups.all()) + + # The AvailabilityManager requires crews to share one of + # the AppForm's Role Groups. + crew = CrewFactory(event=application_form.event, role_group=application_form.role_groups.first(), kind=models.CrewKind.GAME_CREW) + CrewFactory(event=application_form.event, role_group=application_form.role_groups.first(), kind=models.CrewKind.EVENT_CREW) CrewFactory(kind=models.CrewKind.GAME_CREW) am = AvailabilityManager.with_application_form(application_form) @@ -370,8 +376,9 @@ def test_availability_manager__static_crews(db): def test_availability_manager__event_crews(db): application_form = ApplicationFormFactory() - crew = CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) - CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) + application_form.role_groups.set(application_form.event.league.role_groups.all()) + crew = CrewFactory(event=application_form.event, role_group=application_form.role_groups.first(), kind=models.CrewKind.EVENT_CREW) + CrewFactory(event=application_form.event, role_group=application_form.role_groups.first(), kind=models.CrewKind.GAME_CREW) CrewFactory(kind=models.CrewKind.EVENT_CREW) am = AvailabilityManager.with_application_form(application_form) From 812116a8c552833152914b22661b4cb004bd6f0c Mon Sep 17 00:00:00 2001 From: David Reed Date: Tue, 3 Feb 2026 21:46:14 -0700 Subject: [PATCH 22/26] Add more tests --- tests/factories.py | 40 +++++++++++++++---- tests/test_avail.py | 94 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index b1aaf19..2ec3dc1 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -65,6 +65,7 @@ class Meta: class EventFactory(factory.django.DjangoModelFactory): class Meta: model = "stave.Event" + skip_postgeneration_save = True class Params: status_open = factory.Trait(status=EventStatus.OPEN) @@ -79,6 +80,16 @@ class Params: end_date="+30d", ) + @factory.post_generation + def role_groups(obj, create, extracted, **kwargs): + event_role_groups = obj.league.role_groups.all() + obj.role_groups.set( + random.sample( + list(event_role_groups), + random.randrange(1, len(event_role_groups)) if len(event_role_groups) > 1 else 1 + ) + ) + class GameFactory(factory.django.DjangoModelFactory): class Meta: @@ -185,14 +196,23 @@ class Meta: league_template=None, ) - # TODO: applicationformtemplates - # TODO: role groups - questions = factory.RelatedFactoryList( + form_questions = factory.RelatedFactoryList( "tests.factories.QuestionFactory", factory_related_name="application_form", size=lambda: random.randint(1, 10), ) + # TODO: applicationformtemplates + @factory.post_generation + def role_groups(obj, create, extracted, **kwargs): + event_role_groups = obj.event.role_groups.all() + obj.role_groups.set( + random.sample( + list(event_role_groups), + random.randrange(1, len(event_role_groups)) if len(event_role_groups) > 1 else 1 + ) + ) + class QuestionFactory(factory.django.DjangoModelFactory): class Meta: @@ -246,12 +266,16 @@ class Meta: status = models.ApplicationStatus.APPLIED + # Roles @factory.post_generation - def roles(self, create, extracted, **kwargs): - if not create or not extracted: - return - self.roles.add(*extracted) - + def roles(obj, create, extracted, **kwargs): + available_roles = models.Role.objects.filter(role_group__in=obj.form.event.role_groups.all()) + obj.roles.set( + random.sample( + list(available_roles), + random.randrange(1, len(available_roles)) if len(available_roles) > 1 else 1 + ) + ) # Questions diff --git a/tests/test_avail.py b/tests/test_avail.py index 1e29cd3..562ff0f 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -385,10 +385,94 @@ def test_availability_manager__event_crews(db): assert am.event_crews == [crew] -def test_set_assignment__override_crew_ex_nihilo(db): - application_form = ApplicationFormFactory() - application_form.role_groups.set(application_form.event.league.role_groups.all()) - am = AvailabilityManager.with_application_form(application_form) - am.set_assignment(role, crew, user) +def test_applications_by_role_group():... + +def test_role_groups():... + +def test_game_crew_assignments():... + +def test_user_availability():... + +def test_user_event_availability():... +def test_user_static_crew_availability():... +def test_get_game_count_for_user(): ... + +def test_game_counts_by_user(): ... + +def test_get_application_counts(): ... +def test_get_all_applications(): ... + +def test_get_application_entries(): ... + +def test_get_swappable_assignments(): ... + +def test_get_application_by_id(): ... + +def test_get_application_for_user():... + +def test_get_application_for_assignment(): ... + +def test_get_assignment(): ... + +def test_set_assignment__open_slot(db): + application = ApplicationFactory() + role = application.roles.first() + crew = CrewFactory( + event=application.form.event, + role_group=role.role_group + ) + + am = AvailabilityManager.with_application_form(application.form) + am.set_assignment(role, crew, application.user) + + assert crew.get_assignments_by_role_id()[role.id].user == application.user + +def test_set_assignment__replace_existing(db): + application = ApplicationFactory() + role = application.roles.first() + other_application = ApplicationFactory(form=application.form) + other_application.roles.set([role]) + crew = CrewFactory( + event=application.form.event, + role_group=role.role_group + ) + CrewAssignment.objects.create( + user=application.user, + crew=crew, + role=role + ) + + am = AvailabilityManager.with_application_form(application.form) + am.set_assignment(role, crew, other_application.user) + + assert crew.get_assignments_by_role_id()[role.id].user == other_application.user + assert not models.CrewAssignment.objects.filter( + user=application.user + ).exists() + +def test_set_assignment__remove_existing(db): + application = ApplicationFactory() + role = application.roles.first() + other_application = ApplicationFactory(form=application.form) + other_application.roles.set([role]) + crew = CrewFactory( + event=application.form.event, + role_group=role.role_group + ) + models.CrewAssignment.objects.create( + user=application.user, + crew=crew, + role=role + ) + + am = AvailabilityManager.with_application_form(application.form) + am.set_assignment(role, crew, None) + + assert role.id not in crew.get_assignments_by_role_id() + assert not models.CrewAssignment.objects.filter( + user=application.user + ).exists() + +def test_set_crew_assignment(): ... From df737124645feed77c49781cb0cc090ad7e5d31d Mon Sep 17 00:00:00 2001 From: David Reed Date: Tue, 3 Feb 2026 23:00:33 -0700 Subject: [PATCH 23/26] More tests, pytest-xdist --- justfile | 4 +- pyproject.toml | 1 + tests/test_avail.py | 43 +- uv.lock | 946 ++++++++++++++++++++++++++------------------ 4 files changed, 596 insertions(+), 398 deletions(-) diff --git a/justfile b/justfile index 9e5bd9a..48b121e 100644 --- a/justfile +++ b/justfile @@ -35,8 +35,8 @@ seed: migrate uv run manage.py seed # Run tests -test *arguments="": - uv run pytest {{arguments}} +test arguments="": + uv run pytest -n logical {{arguments}} # Run behavioral tests behave arguments="": diff --git a/pyproject.toml b/pyproject.toml index 7029453..ea23eb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "django-meta>=2.5.0", "django-ftl>=0.14", "django-storages[s3]>=1.14.6", + "pytest-xdist[psutil]>=3.8.0", ] [dependency-groups] diff --git a/tests/test_avail.py b/tests/test_avail.py index 562ff0f..cb9a34d 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -417,7 +417,10 @@ def test_get_application_for_assignment(): ... def test_get_assignment(): ... def test_set_assignment__open_slot(db): - application = ApplicationFactory() + application = ApplicationFactory( + status=models.ApplicationStatus.APPLIED, + form__application_kind=models.ApplicationKind.ASSIGN_ONLY + ) role = application.roles.first() crew = CrewFactory( event=application.form.event, @@ -428,17 +431,25 @@ def test_set_assignment__open_slot(db): am.set_assignment(role, crew, application.user) assert crew.get_assignments_by_role_id()[role.id].user == application.user + application.refresh_from_db() + assert application.status == models.ApplicationStatus.ASSIGNMENT_PENDING def test_set_assignment__replace_existing(db): - application = ApplicationFactory() + application = ApplicationFactory( + status=models.ApplicationStatus.ASSIGNMENT_PENDING, + form__application_kind=models.ApplicationKind.ASSIGN_ONLY + ) role = application.roles.first() - other_application = ApplicationFactory(form=application.form) + other_application = ApplicationFactory( + form=application.form, + status=models.ApplicationStatus.APPLIED + ) other_application.roles.set([role]) crew = CrewFactory( event=application.form.event, role_group=role.role_group ) - CrewAssignment.objects.create( + models.CrewAssignment.objects.create( user=application.user, crew=crew, role=role @@ -451,12 +462,17 @@ def test_set_assignment__replace_existing(db): assert not models.CrewAssignment.objects.filter( user=application.user ).exists() + application.refresh_from_db() + assert application.status == models.ApplicationStatus.APPLIED + other_application.refresh_from_db() + assert other_application.status == models.ApplicationStatus.ASSIGNMENT_PENDING def test_set_assignment__remove_existing(db): - application = ApplicationFactory() + application = ApplicationFactory( + status=models.ApplicationStatus.ASSIGNMENT_PENDING, + form__application_kind=models.ApplicationKind.ASSIGN_ONLY + ) role = application.roles.first() - other_application = ApplicationFactory(form=application.form) - other_application.roles.set([role]) crew = CrewFactory( event=application.form.event, role_group=role.role_group @@ -474,5 +490,18 @@ def test_set_assignment__remove_existing(db): assert not models.CrewAssignment.objects.filter( user=application.user ).exists() + application.refresh_from_db() + assert application.status == models.ApplicationStatus.APPLIED + +def test_set_assignment__swap_roles_override_crew(db):... + + +def test_set_assignment__swap_roles_static_crew_to_override_crew(db): ... +def test_set_assignment__swap_roles_static_crew_to_blank(db): ... +def test_set_assignment__swap_roles_event_crew(db): ... + +def test_set_assignment__swap_roles_static_crew_to_override_crew_keep_nonexclusive(db): ... +def test_set_assignment__swap_roles_event_crew_keep_nonexclusive(db): ... +def test_set_assignment__static_crew_override_replace_blank_with_original(db): ... def test_set_crew_assignment(): ... diff --git a/uv.lock b/uv.lock index cdd3fff..ad23e2a 100644 --- a/uv.lock +++ b/uv.lock @@ -7,59 +7,59 @@ prerelease-mode = "if-necessary" [[package]] name = "apscheduler" -version = "3.11.0" +version = "3.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzlocal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, ] [[package]] name = "asgiref" -version = "3.8.1" +version = "3.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "beautifulsoup4" -version = "4.13.5" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "behave" -version = "1.2.7.dev6" +version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -69,35 +69,35 @@ dependencies = [ { name = "parse-type" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/b7/be3a5fb5ed211c6a4833989197879c07845019faa59eba0106482e8e16ac/behave-1.2.7.dev6.tar.gz", hash = "sha256:6e8791901c518bdbd41a99df6574127f53d9cb651e451268d3c50741941de0bf", size = 827062, upload-time = "2024-09-24T04:38:57.411Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/51/f37442fe648b3e35ecf69bee803fa6db3f74c5b46d6c882d0bc5654185a2/behave-1.3.3.tar.gz", hash = "sha256:2b8f4b64ed2ea756a5a2a73e23defc1c4631e9e724c499e46661778453ebaf51", size = 892639, upload-time = "2025-09-04T12:12:02.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/89/93f6e97a3b2ba68de26b42a67ba03f9aa92f2691a7724d9c86137b1266f3/behave-1.2.7.dev6-py2.py3-none-any.whl", hash = "sha256:b4f1535249414f6cca544870f62ca7291002eedb66ddc154cff20b363bd77e09", size = 201938, upload-time = "2024-09-24T04:38:55.4Z" }, + { url = "https://files.pythonhosted.org/packages/63/71/06f74ffed6d74525c5cd6677c97bd2df0b7649e47a249cf6a0c2038083b2/behave-1.3.3-py2.py3-none-any.whl", hash = "sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0", size = 223594, upload-time = "2025-09-04T12:12:00.3Z" }, ] [[package]] name = "behave-django" -version = "1.7.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "behave" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/18/f048fa69fda88943f317455ab439ce9c32960c1ba771e05e0bd68f3a41d6/behave_django-1.7.0.tar.gz", hash = "sha256:e253a4abc748559a0f9ad019349244fb0e4287f9cc70790dd0d20f3daf23c19e", size = 16355, upload-time = "2025-07-18T00:37:15.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/94/4976841a5a956357b91e1f73e55d885139d62973874e08de3ac9293dcb72/behave_django-1.9.0.tar.gz", hash = "sha256:cbae403b9c9873d4d079f3560d6c8b390eaea9fb8e8caff63439a0b06ea60309", size = 16313, upload-time = "2025-11-15T14:04:21.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/c1/71406da03fa7ea3561cec5b879fd444fb158ac66120cb4d30df913d88506/behave_django-1.7.0-py3-none-any.whl", hash = "sha256:ab842fec0ed267cfc77d209a5fad574a152f247f798cec0eb18a9f9480995d1f", size = 12535, upload-time = "2025-07-18T00:37:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/8d/f9a54742833f136dbf396bbbdb9aa0f62ddaf3f1621d772abecf5eac8cf6/behave_django-1.9.0-py3-none-any.whl", hash = "sha256:41c6d95886cd59413493037cc79fbe3346a828c83594431bbd596859ad12399b", size = 12233, upload-time = "2025-11-15T14:04:20.607Z" }, ] [[package]] name = "bleach" -version = "6.2.0" +version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, ] [package.optional-dependencies] @@ -107,83 +107,141 @@ css = [ [[package]] name = "boto3" -version = "1.38.46" +version = "1.42.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/56/ca67d22364ce8f23b1885d9a1bae50d8005fa9b32ec0deddba0b19079b99/boto3-1.38.46.tar.gz", hash = "sha256:d1ca2b53138afd0341e1962bd52be6071ab7a63c5b4f89228c5ef8942c40c852", size = 111883, upload-time = "2025-06-27T20:18:17.096Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/2d/69fb3acd50bab83fb295c167d33c4b653faeb5fb0f42bfca4d9b69d6fb68/boto3-1.42.96.tar.gz", hash = "sha256:b38a9e4a3fbbee9017252576f1379780d0a5814768676c08df2f539d31fcdd68", size = 113203, upload-time = "2026-04-24T19:47:18.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/13/0eb850c821a976346a905370bb30c86da7edc2bbc3977c445fffc6306032/boto3-1.38.46-py3-none-any.whl", hash = "sha256:9c8e88a32a6465e5905308708cff5b17547117f06982908bdfdb0108b4a65079", size = 139925, upload-time = "2025-06-27T20:18:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b3f617d011c42eb804d993103b8fa9acdce153e181a3042f58bfe33d7cb4/boto3-1.42.96-py3-none-any.whl", hash = "sha256:2f4566da2c209a98bdbfc874d813ef231c84ad24e4f815e9bc91de5f63351a24", size = 140557, upload-time = "2026-04-24T19:47:15.824Z" }, ] [[package]] name = "botocore" -version = "1.38.46" +version = "1.42.96" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/5f/d76870e4399fbfc12aa5c3bb36029edfc1a434392afc70a343c9d7d96e90/botocore-1.38.46.tar.gz", hash = "sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e", size = 14074340, upload-time = "2025-06-27T20:18:06.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/77/2c333622a1d47cf5bf73cdcab0cb6c92addafbef2ec05f81b9f75687d9e5/botocore-1.42.96.tar.gz", hash = "sha256:75b3b841ffacaa944f645196655a21ca777591dd8911e732bfb6614545af0250", size = 15263344, upload-time = "2026-04-24T19:47:05.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/00/dd90b7a0255587ba1c9754d32a221adb4a9022f181df3eef401b0b9fadfc/botocore-1.38.46-py3-none-any.whl", hash = "sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b", size = 13736872, upload-time = "2025-06-27T20:18:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/152c3a859ca1b9d77ed16deac3cf81682013677c68cf5715698781fc81bd/botocore-1.42.96-py3-none-any.whl", hash = "sha256:db2c3e2006628be6fde81a24124a6563c363d6982fb92728837cf174bad9d98a", size = 14945920, upload-time = "2026-04-24T19:47:00.323Z" }, ] [[package]] name = "certifi" -version = "2025.6.15" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -197,173 +255,199 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, - { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, - { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, - { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, - { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, - { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, - { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, - { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, - { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, - { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, - { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, - { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, - { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, - { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, - { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, - { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] name = "cryptography" -version = "45.0.4" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" }, - { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, - { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, - { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, - { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, - { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" }, - { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" }, - { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, - { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, - { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, - { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, - { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, - { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, - { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, ] [[package]] name = "cucumber-expressions" -version = "18.0.1" +version = "19.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/7d/f4e231167b23b3d7348aa1c90117ce8854fae186d6984ad66d705df24061/cucumber_expressions-18.0.1.tar.gz", hash = "sha256:86ce41bf28ee520408416f38022e5a083d815edf04a0bd1dae46d474ca597c60", size = 22232, upload-time = "2024-10-28T11:38:48.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/5f/1afc1a0a2a6daed47b2d032a897613a556ebf49303e4af8310223f4a450b/cucumber_expressions-19.0.0.tar.gz", hash = "sha256:8eb5ae46dd03dd37fec1163ace1510529501d7d1868ff372c1ab2cd5aa4543a8", size = 13722, upload-time = "2026-01-25T18:09:15.642Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/e0/31ce90dad5234c3d52432bfce7562aa11cda4848aea90936a4be6c67d7ab/cucumber_expressions-18.0.1-py3-none-any.whl", hash = "sha256:86230d503cdda7ef35a1f2072a882d7d57c740aa4c163c82b07f039b6bc60c42", size = 20211, upload-time = "2024-10-28T11:38:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/3b/72/eb79377be899d24c91ed196a50808563685992bb3aa6b82dbe3a1e30df67/cucumber_expressions-19.0.0-py3-none-any.whl", hash = "sha256:f452e6c73258c1677043ad67ad5f538c87284d6b502004720510fb6b7452d9c5", size = 20232, upload-time = "2026-01-25T18:09:16.763Z" }, ] [[package]] name = "cucumber-tag-expressions" -version = "6.2.0" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/81/32a2dc51c0720b34f642a6e79da6d89525c1eafd8902798026c233201f6f/cucumber_tag_expressions-6.2.0.tar.gz", hash = "sha256:b60aa2cdbf9ac43e28d9b0e4fd49edf9f09d5d941257d2912f5228f9d166c023", size = 41459, upload-time = "2025-05-25T12:30:43.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/e0/de0b292a533846def28a4373a00c883ffa5ed986ca79f0284bd69a6297b8/cucumber_tag_expressions-9.1.0.tar.gz", hash = "sha256:d960383d5885300ebcbcb14e41657946fde2a59d5c0f485eb291bc6a0e228acc", size = 8437, upload-time = "2026-02-17T21:59:06.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/99/0e9ac5b8429f39a05de5cd4731eac57738ce030dcd852aefe36a7102a4ce/cucumber_tag_expressions-6.2.0-py2.py3-none-any.whl", hash = "sha256:f94404b656831c56a3815da5305ac097003884d2ae64fa51f5f4fad82d97e583", size = 9333, upload-time = "2025-05-25T12:30:41.408Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cf/8e8d034f7d55fceb2e4765bf9fab5da6d6a09204cd09de7bb5054f242cd0/cucumber_tag_expressions-9.1.0-py3-none-any.whl", hash = "sha256:cca145d677a942c1877e5a2cf13da8c6ec99260988877c817efd284d8455bb56", size = 9726, upload-time = "2026-02-17T21:59:04.755Z" }, ] [[package]] name = "dj-database-url" -version = "3.0.0" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/c0/5d660bbe707c8bce8c9217c9af92345170c204e29005628dc57528b648d7/dj_database_url-3.0.0.tar.gz", hash = "sha256:749a7a42d88d6c741c1d2f4ab24c2ae0d5cd12f00f2d1d55ff9f5fadabe8a2c3", size = 12594, upload-time = "2025-06-02T08:50:42.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/f6/00b625e9d371b980aa261011d0dc906a16444cb688f94215e0dc86996eb5/dj_database_url-3.1.2.tar.gz", hash = "sha256:63c20e4bbaa51690dfd4c8d189521f6bf6bc9da9fcdb23d95d2ee8ee87f9ec62", size = 11490, upload-time = "2026-02-19T15:30:23.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/7b/d68df6365e442ae6370d6d970915329eae85bce5afb3602058d9ccc71700/dj_database_url-3.0.0-py3-none-any.whl", hash = "sha256:cbb84b2e3f372460b1e43692bf9fdc0c32e78930ee101db470cba56105fca1e5", size = 8835, upload-time = "2025-06-02T08:50:52.056Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" }, ] [[package]] name = "djade" -version = "1.7.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/31/05baa6b0419c6cd13e4ecae275aadfb6d5c0b332f1b06d4fccb4cb4a2b29/djade-1.7.0.tar.gz", hash = "sha256:b49e7ad42050ecac10ae990a6adbecec08c62b0a32a33de748d63d948bced815", size = 39433, upload-time = "2025-12-10T16:03:53.618Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/2e/655cc12eefa761d54b9cb956b8650f83675fd144fda641b03d4172d2e0c2/djade-1.9.0.tar.gz", hash = "sha256:e07dcec03982af3f12e46e42661c83ba6835edbde3597e0385ecfe7385527721", size = 41968, upload-time = "2026-02-27T00:07:02.57Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/41/d25b88a4ff05a495aaf992793eae116b867e6a4ebda64701f737f22abf18/djade-1.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:084f505338862542343d4cabbd52a8e58a8b05cc1a0eb37d78824073485d4c8d", size = 1156096, upload-time = "2025-12-10T16:03:35.626Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/8ca54f7d4ab2c26095f938cedb13d2508032d9eadcfd07757a46a02cc38a/djade-1.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9676b72b9e9e494d977f887cda825e333e699ae7184384795d5a9e2e5d2c7f69", size = 1097385, upload-time = "2025-12-10T16:03:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5d/781d7204973cd12138805d4a6b67ec7666771b66709242f6b7b6834f862b/djade-1.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0102aaf6f1a259f2ca78839c796b130fc53868965b01fb0b8d5768c5fe142b99", size = 1142211, upload-time = "2025-12-10T16:03:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/e9/aa/ba674b4a78be552f9bba496369f0a436b0eebc0bfc7c41bd250776428967/djade-1.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db11d4563b73c3eacd2433005e9467d67c3a58c61ce6b5d821b89b124d1dd168", size = 1077205, upload-time = "2025-12-10T16:03:41.001Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2d/30f9ce233e02e9e29793b8eb2784a09d5d18b8aa6bbb53374403cee39ac8/djade-1.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa4659a2c3c172cf0ed35d19cb2d396248e6889b47b4ae52263be1d89f0a532", size = 1264698, upload-time = "2025-12-10T16:03:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/6a/88/a5a79f2e57e13c0403a238173744d3eb08f03616f0e399a6fe88a806bf66/djade-1.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9596896b44e25fbd897e0e8d134a108184f56c32feff8607028daa2315506f47", size = 1217375, upload-time = "2025-12-10T16:03:44.908Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a2/73839c8d4f9e96ed3f4a4e6e51cc2934fdbc1f1f4ff8be1effbdb7574fe5/djade-1.7.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:7fbdfece645f5816adae816eed847db1905d1e14cba3da5384706adce1eefb2a", size = 1139307, upload-time = "2025-12-10T16:03:46.599Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a1/462b4e004c5fc9ce9d4f93968e8ee5ba646c033f95e05fc70d8864ac3cb8/djade-1.7.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:51221cff697e1eaa709e375ecac331366ec68cd90567de36f4bd3daca94e8327", size = 1277128, upload-time = "2025-12-10T16:03:47.997Z" }, - { url = "https://files.pythonhosted.org/packages/0c/54/8b2fc833d1fdeb08d9e87a1db7dd8d4b64854b9d62c7867b69519c357f21/djade-1.7.0-py3-none-win32.whl", hash = "sha256:20308b03c51bd2551137f5852afb663ac5f86015321cc4f45363427f7dbd9534", size = 988134, upload-time = "2025-12-10T16:03:49.264Z" }, - { url = "https://files.pythonhosted.org/packages/f9/dd/fea876ed43f13182a6381c56dbe8daf4ed4d745228292c38ea44e85ea56d/djade-1.7.0-py3-none-win_amd64.whl", hash = "sha256:a1d41819a0409c74e0e521e856799cbce38c50a1bd5b020fa209a40f9441b499", size = 1098723, upload-time = "2025-12-10T16:03:50.788Z" }, - { url = "https://files.pythonhosted.org/packages/2c/51/438fcae17c2afa62966f0fbda3799f3bada7110d92cc3c58ebc47b636925/djade-1.7.0-py3-none-win_arm64.whl", hash = "sha256:fef26be22c5b3c15e6b8be3888ae0672aab5067d219cf2be145af22860757c08", size = 1028482, upload-time = "2025-12-10T16:03:52.098Z" }, + { url = "https://files.pythonhosted.org/packages/75/1c/40f332f13fc63495677aca02a22a74e5dd596f85d762fd34dcad7177a878/djade-1.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9523c5eebf67bdf4037cbaa3c79fbb453ee883c370e772f2dbb767c753971ac7", size = 1167298, upload-time = "2026-02-27T00:06:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/8f/02/2098990d681265e84e8d0d2b4548279dcaae07195d217f71e799f4006025/djade-1.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8da28ee6ac2e163283c1f19965f04cb4ca1e4e9fd9edc4c1f4cf183cab98384e", size = 1106315, upload-time = "2026-02-27T00:06:48.756Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/e6f43a63e9c6297e3117d5e8415c78c64f4403cace455641b6eb6590d7e5/djade-1.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbfc70ad912b54329363de7a5b62afd6bf4b92a49c12e2a155be1138cef01424", size = 1150821, upload-time = "2026-02-27T00:06:50.551Z" }, + { url = "https://files.pythonhosted.org/packages/90/8d/cb656b034477b734adefa69c7ada69d17bde4a7bf5a69e797f53ac59ecc3/djade-1.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9d828aaa871c02655d0a6b2d9fd2a677cd4b16bd101015728dd5db418b8225b", size = 1085847, upload-time = "2026-02-27T00:06:52.372Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/59396b03db75dd14f74402adb7348362863fde05313f643542dc24ae665e/djade-1.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b90e709d21c3f953b52fc5a021b8ef69b0790f23aa308923992af1fc3c2353f", size = 1228570, upload-time = "2026-02-27T00:06:54.167Z" }, + { url = "https://files.pythonhosted.org/packages/d3/22/6713dd41f6c6615bf6f11f552286f2ff0e56e4eb0050f6223d1932b38678/djade-1.9.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:183b828ffcedb0122bcf0b5eaa008bb288c344a80946c3f372d583f3944c1394", size = 1147326, upload-time = "2026-02-27T00:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/190854396549d3741b70fd5f828ca0cfeee203e7a6219500cc9bc97bab76/djade-1.9.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1981f77ef3c8b73860e492918ce235e46ab4e62a63c8101dd379e204c8a162de", size = 1287672, upload-time = "2026-02-27T00:06:57.421Z" }, + { url = "https://files.pythonhosted.org/packages/70/dd/bbbf681b4918fd178bd6586b28146af55e8f0c833e6ce26f255fe4f0271a/djade-1.9.0-py3-none-win_amd64.whl", hash = "sha256:bb2b62e22971173dae63bb2d18b24fe85bf3470ca1aabf1b8320a578832d64a2", size = 1110197, upload-time = "2026-02-27T00:06:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/e39757938c1387c29b27e9362f3b567a8f6b24bd223137a11253b0c315e7/djade-1.9.0-py3-none-win_arm64.whl", hash = "sha256:b90633a53bb996e0fffd9bd26ed995a78b15f368aea77348e35e67151de4fd13", size = 1041427, upload-time = "2026-02-27T00:07:01.007Z" }, ] [[package]] name = "django" -version = "5.2.11" +version = "5.2.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" }, ] [[package]] name = "django-allauth" -version = "65.9.0" +version = "65.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/a3/00aa9d5bb5df4f7464495675074dc11107c08b3eea3462fb3edc059d71e1/django_allauth-65.9.0.tar.gz", hash = "sha256:a06bca9974df44321e94c33bcf770bb6f924d1a44b57defbce4d7ec54a55483e", size = 1710514, upload-time = "2025-06-01T19:21:07.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/df/357187dfff18c7783e4911827a6c69437e290d7259a32a99c23fcd85997f/django_allauth-65.16.1.tar.gz", hash = "sha256:4425ac3088541c4c54983e16e08f6e3eb9f438dc1b1009534fa51c8bb739ed31", size = 2232835, upload-time = "2026-04-17T18:53:59.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/58/d95b6c3088d83697bfd93782ee57bc6a6462e41eb19121a947b8a015396a/django_allauth-65.16.1-py3-none-any.whl", hash = "sha256:e49df24056bf37c44e56aaad1e51f78994b7d175bc3476d65e8f8f58390a8ce8", size = 2051868, upload-time = "2026-04-17T18:54:12.032Z" }, +] [package.optional-dependencies] mfa = [ @@ -371,23 +455,24 @@ mfa = [ { name = "qrcode" }, ] socialaccount = [ + { name = "oauthlib" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, - { name = "requests-oauthlib" }, ] [[package]] name = "django-anymail" -version = "13.0" +version = "15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, + { name = "idna" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/8f/c5c8e0c952797247c763edfd02346a9f989489d54a3debd717ecb0abb55a/django_anymail-13.0.tar.gz", hash = "sha256:87f42d9ff12a9a029d5e88edaaf62a4b880aa9a6a7ef937042b7e96579e6db07", size = 94298, upload-time = "2025-04-03T21:04:44.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/43/f0aadb31f2c58afcd9f001f4291998cbd6d289898167e79d908506fc6faf/django_anymail-15.0.tar.gz", hash = "sha256:23d8ab6589afe8cc1ae7665c26879814ad192f4c3ed837a2a1868b0a056869e0", size = 106985, upload-time = "2026-04-18T20:44:19.237Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/32/c9691a478ca0460131c2f0bfadbb0e20cd316588aa24f809aebdaa7a79b2/django_anymail-13.0-py3-none-any.whl", hash = "sha256:6da4465eff18f679955f74332501a3a4299e34079015d91e1fce9c049d784d6c", size = 130285, upload-time = "2025-04-03T21:04:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/75/d1/daae99ec3b30886010a499975880ec20c32c622bee6b92c226b715e42f0c/django_anymail-15.0-py3-none-any.whl", hash = "sha256:64d33dd1084bfc8e4e12245f56629be40aa0b0498fc7fc7544d87b9b2048be1e", size = 147229, upload-time = "2026-04-18T20:44:17.323Z" }, ] [package.optional-dependencies] @@ -410,28 +495,28 @@ wheels = [ [[package]] name = "django-coverage-plugin" -version = "3.2.0" +version = "3.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/d7/4e104b50911d1328e5cc26e89feca60ed0f12ea9b5f7e8ce776ce26d84c8/django_coverage_plugin-3.2.0.tar.gz", hash = "sha256:0e1460294ecd4b192bd09788ab9ad9380d9b8c9b45925b408ce6c620ac352585", size = 29252, upload-time = "2025-10-05T22:42:05.337Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/07/c3814563d63b8680f4d5bb8880bb151039e258e7d91a7867b0eaf45165bd/django_coverage_plugin-3.2.2.tar.gz", hash = "sha256:fcc507ee02f3a8f7c1d79b6eba1bafebb4a95b5055801987b715f4f88270c441", size = 30814, upload-time = "2026-04-04T20:43:22.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/1c/73ff697998143eab2f4f0dbd79da7d7b8aa821d47cbc9bb26eab0a9283aa/django_coverage_plugin-3.2.0-py3-none-any.whl", hash = "sha256:a4a9400c784c86f1ba53a73c336508e07316c92345b34a0eb0b22b3b14cdbdd6", size = 14498, upload-time = "2025-10-05T22:42:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/981522be2a8c46eebdd70debcbb2b0e55a8dbd44a3034c727e298b2ba129/django_coverage_plugin-3.2.2-py3-none-any.whl", hash = "sha256:66c9bdb2756762d6bef3510548bb228e1e8465ff1cd2b372775b82624e85e0b4", size = 14463, upload-time = "2026-04-04T20:43:20.849Z" }, ] [[package]] name = "django-debug-toolbar" -version = "5.2.0" +version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "sqlparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/9f/97ba2648f66fa208fc7f19d6895586d08bc5f0ab930a1f41032e60f31a41/django_debug_toolbar-5.2.0.tar.gz", hash = "sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821", size = 297901, upload-time = "2025-04-29T05:23:57.533Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/ea/b62673424dd72d2dbf5adf4145281a421d5792f47380d9bc8e3b11e1a769/django_debug_toolbar-6.3.0.tar.gz", hash = "sha256:f830a86fe02e17f625a22cfbed24a5bd1500762e201ec959c50efb0f9327282b", size = 334079, upload-time = "2026-04-02T16:07:01.385Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/c2/ed3cb815002664349e9e50799b8c00ef15941f4cad797247cadbdeebab02/django_debug_toolbar-5.2.0-py3-none-any.whl", hash = "sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195", size = 262834, upload-time = "2025-04-29T05:23:55.472Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9e/d8c3c845f4b5ccac7377c19f4049e7e00c6f121846a81f69a497b45734df/django_debug_toolbar-6.3.0-py3-none-any.whl", hash = "sha256:a199ce3d0f884739a9096835ad417479fede05f3b3c4824bc8b354721ba8f629", size = 298304, upload-time = "2026-04-02T16:06:59.617Z" }, ] [[package]] @@ -461,15 +546,15 @@ wheels = [ [[package]] name = "django-htmx" -version = "1.23.2" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/d4/1709593b01799c29c8d5a6f72973d787c8b881b0f8e2767b9e913fe2c688/django_htmx-1.23.2.tar.gz", hash = "sha256:65a8c8825fcae983b94aedce26af96a70717ab185d55cdb8a7a4bb68863ab079", size = 64415, upload-time = "2025-06-27T14:09:32.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f2/8c3e28a5eed8e5226835c762892bfef74eda7e8629c65b49c186098eb303/django_htmx-1.27.0.tar.gz", hash = "sha256:036e5da801bfdf5f1ca815f21592cfb9f004a898f330c842f15e55c70e301a75", size = 65362, upload-time = "2025-11-28T23:18:55.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/84/d5b29c102743abd61e1852b619263c8938b8516c71a138f4053114270743/django_htmx-1.23.2-py3-none-any.whl", hash = "sha256:c288fe92bdcfa7c2ed9665d6d23cc55c6693a7cc8d22cf0a01e1e38318874030", size = 61503, upload-time = "2025-06-27T14:09:31.272Z" }, + { url = "https://files.pythonhosted.org/packages/23/ac/25d28489dc43224e260f4ebee7565f7ef1efe12af0f284a89500c19f75e2/django_htmx-1.27.0-py3-none-any.whl", hash = "sha256:13e1e13b87d39b57f95aae6e4987cb3df056d0b1373a41f4a94504a00298ffd8", size = 62126, upload-time = "2025-11-28T23:18:53.57Z" }, ] [[package]] @@ -488,38 +573,38 @@ wheels = [ [[package]] name = "django-markdownify" -version = "0.9.5" +version = "0.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bleach", extra = ["css"] }, { name = "django" }, { name = "markdown" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/33/3abb966e2b238af4c9a5d3ee38a7aa7e51b644b4b20bf8533b6fd1c1bf96/django_markdownify-0.9.5.tar.gz", hash = "sha256:34c34eba4a797282a5c5bd97b13cec84d6a4c0673ad47ce1c1d000d74dd8d4ab", size = 7939, upload-time = "2024-05-09T11:45:27.661Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/65/f3630c4cc20511b9c9f222a9005373a7fbd303e3233befd7a163200e631a/django_markdownify-0.9.6.tar.gz", hash = "sha256:edcf47b2026d55a8439049d35c8b54e11066a4856c4fad1060e139cb3d2eee52", size = 8069, upload-time = "2025-12-06T10:25:19.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/35/c7a4bd957b279a8e7c808116bed399b73874ed3da78689993ee76f30d9f6/django_markdownify-0.9.5-py3-none-any.whl", hash = "sha256:2c4ae44e386c209453caf5e9ea1b74f64535985d338ad2d5ad5e7089cc94be86", size = 10342, upload-time = "2024-05-09T11:45:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/67/e1/fafee8ffd7b5a1c4fd5eaf303a00c8bbbf38a90eae940823146c38b9a188/django_markdownify-0.9.6-py3-none-any.whl", hash = "sha256:9863b2bfa6d159ad1423dc93bf0d6eadc6413776de304049aa9fcfa5edd2ce1c", size = 10449, upload-time = "2025-12-06T10:25:17.818Z" }, ] [[package]] name = "django-meta" -version = "2.5.0" +version = "2.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/07/fcbaa9b9a067f1717813534b1193b021c544b1f3fb5bfb8f312eca232d95/django_meta-2.5.0.tar.gz", hash = "sha256:e30669865bccff6be61765dfc57d97ee0a68ab7625efd081dd9045e17f3500c5", size = 29111, upload-time = "2025-04-18T13:15:28.913Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/a3/4cf42f67c44e193db994aad15732aedce1e94e94ab21dcfe93e233b1cf40/django_meta-2.5.1.tar.gz", hash = "sha256:0699e585444d59286c25bc301b865e56814e2473a069cd1046840669204cec2a", size = 29308, upload-time = "2026-02-17T10:33:29.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/ba/46f611931e00a5e9e0ac8fc6f28d3317a77894f07f7db637111c32dc1d79/django_meta-2.5.0-py2.py3-none-any.whl", hash = "sha256:94674286c8515314a025958af6bbcb44d6134d5a2ea3a61f680a852d334af88d", size = 27892, upload-time = "2025-04-18T13:15:27.677Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3d/d839a42f52187c05abe9b8e517a98a71baff5241222dfb36792d2818c8c6/django_meta-2.5.1-py2.py3-none-any.whl", hash = "sha256:35941347c39fa69139cdede3fe39f408330eb586819c7a9c4610991479e1ae57", size = 27935, upload-time = "2026-02-17T10:33:28.124Z" }, ] [[package]] name = "django-recurrence" -version = "1.11.1" +version = "1.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/c7/e8d8539d8ccb3ee6498206b6ecef6cd551c3a281e28ae16812b9bc868da3/django-recurrence-1.11.1.tar.gz", hash = "sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70", size = 133440, upload-time = "2022-01-25T10:14:06.734Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/90/f083ae5fae92c121ed26689c21bba68cb4651defe62bdb70200ef47ece06/django_recurrence-1.14.tar.gz", hash = "sha256:154a6221bd6667c35250d9fa89bbc4792b3f1ec5dc8dff0f5872186b6df3cf76", size = 102449, upload-time = "2025-12-19T21:30:44.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/0a/5dc0f1f408a5b5f00c64ec57f3868248ed65ee660980118867b1279eca96/django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5", size = 127256, upload-time = "2022-01-25T10:14:05.319Z" }, + { url = "https://files.pythonhosted.org/packages/85/08/fec7dd78259512152c75e8c64ac5ba389eef55569915b190f6b798f6ff7a/django_recurrence-1.14-py3-none-any.whl", hash = "sha256:3e7420a38c7fa2f5073598e2a4236c65a983213f30f4413618448b43514649cd", size = 135711, upload-time = "2025-12-19T21:30:43.599Z" }, ] [[package]] @@ -541,39 +626,39 @@ s3 = [ [[package]] name = "django-stubs-ext" -version = "5.2.1" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/19/bf8cc0e6c48b4bf4fb72274cecc6b575a4789effecdd95d9ccc7ba9380bb/django_stubs_ext-5.2.1.tar.gz", hash = "sha256:fc0582cb3289306c43ce4a0a15af86922ce1dbec3c19eab80980ee70c04e0392", size = 6550, upload-time = "2025-06-17T18:06:59.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/e6/5dcdaa785ec3eed5fc196c7e68fb7ad9d9fe6d5acccea4690e65f2546417/django_stubs_ext-6.0.3.tar.gz", hash = "sha256:3307d42132bc295d5744de6276bc5fdf6896efc70f891e21c0ae8bdf529d2762", size = 6663, upload-time = "2026-04-18T15:10:53.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/82/9fab66569b3e682205b52c2b203058a816c0755bc54e2adcd5d3f6018c43/django_stubs_ext-5.2.1-py3-none-any.whl", hash = "sha256:98fb0646f1a1ef07708eec5f6f7d27523f12c0c8714abae8db981571ff957588", size = 9153, upload-time = "2025-06-17T18:06:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/10/fa/0a3a05c29d6295dbd52fa3cb4047a95de11ba4f2696072d6f3f2c1e6f370/django_stubs_ext-6.0.3-py3-none-any.whl", hash = "sha256:9e4105955419ae310d7da9cfd808e039d4dae3092c628f021057bb4f2c237f8f", size = 10354, upload-time = "2026-04-18T15:10:52.395Z" }, ] [[package]] name = "django-template-partials" -version = "24.4" +version = "25.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ff/2a7ddae12ca8e5fea1a41af05924c04f1bb4aec7157b04a88b829dd93d4a/django_template_partials-24.4.tar.gz", hash = "sha256:25b67301470fc274ecc419e5e5fd4686a5020b1c038fd241a70eb087809034b6", size = 14538, upload-time = "2024-08-16T10:51:30.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/2e/957ee4a6ee0a7d46a18676ba8b01d762ef89d00b7769cc532853f9f989e1/django_template_partials-25.3.tar.gz", hash = "sha256:6d11f7bb049ce3032e6fe3331137b771e34239ce1af18c55ef6a9b667cf2ef36", size = 18052, upload-time = "2025-11-14T08:27:21.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/72/d8eea70683b25230e0d2647b5cf6f2db4a7e7d35cb6170506d9618196374/django_template_partials-24.4-py2.py3-none-any.whl", hash = "sha256:ee59d3839385d7f648907c3fa8d5923fcd66cd8090f141fe2a1c338b917984e2", size = 8439, upload-time = "2024-08-16T10:51:28.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/48f8721e48b938ca2e2dde577986624543be6ff9bdccac20ccb747be4287/django_template_partials-25.3-py2.py3-none-any.whl", hash = "sha256:a19334934cf40e4e1218802a4ddfdf22b8f78cc5a0b8c75a18b97e6ea4f3c108", size = 9702, upload-time = "2025-11-14T08:27:20.243Z" }, ] [[package]] name = "django-types" -version = "0.21.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-psycopg2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b2/53ecd0877fb35dce7ef6054855d6dd7a4c052dbd1b908ae23d05c0c69c5b/django_types-0.21.0.tar.gz", hash = "sha256:db9e3152019fa2824130f043339d2c77ef156f77cf4a8a8f0d91a1a94fc61b59", size = 164515, upload-time = "2025-06-14T15:35:25.883Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/7b/8e05b8631fa7de84038d4f24fa57e983107aff889a6d88a9c40a21e15d1c/django_types-0.24.0.tar.gz", hash = "sha256:af903de8b9ee963b7594459a7a20cb8eaaab176ae2b3244ecaa089e0c570b0d1", size = 208426, upload-time = "2026-04-22T22:19:01.999Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/f4/ebc5ea60ae7b840a48f3777905b859ebc68b60128b833debd8623acc5c58/django_types-0.21.0-py3-none-any.whl", hash = "sha256:4ebb09b045c69cc8c950a7fc0ba7d62d8e4f8f7599ac3adfad0b6a1f755f407a", size = 374446, upload-time = "2025-06-14T15:35:24.535Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ab/f5c37ecc08c396df62579246597796697b58253c8b30206870e8d0f644d0/django_types-0.24.0-py3-none-any.whl", hash = "sha256:ddb478ca733e0dde5475118dd59ab340156980f9659fd92de2083326ae96100a", size = 379436, upload-time = "2026-04-22T22:19:03.368Z" }, ] [[package]] @@ -587,9 +672,18 @@ wheels = [ [[package]] name = "editorconfig-checker" -version = "3.6.0" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/c0/44142a174310c63f2e36ec288fc02df3d1572e00ed1bb87de7d49d65e412/editorconfig_checker-3.6.1.tar.gz", hash = "sha256:7b6285cfa0797c1f4e1ecae6ec218ff440cdbd77a2e0aa95d9ee3e113e79fd5e", size = 4772, upload-time = "2026-03-04T20:04:36.125Z" } + +[[package]] +name = "execnet" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/33/00730df977c6d3b18441f7726f503036db678ed089b10710ed6b6dd299a3/editorconfig_checker-3.6.0.tar.gz", hash = "sha256:dcaf0248ad7e8539fda03857c300267a4af004e0c2aaa195485e6be85034f6d0", size = 4769, upload-time = "2025-11-30T14:39:19.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] [[package]] name = "factory-boy" @@ -605,26 +699,26 @@ wheels = [ [[package]] name = "faker" -version = "37.8.0" +version = "40.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tzdata" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/da/1336008d39e5d4076dddb4e0f3a52ada41429274bf558a3cc28030d324a3/faker-37.8.0.tar.gz", hash = "sha256:090bb5abbec2b30949a95ce1ba6b20d1d0ed222883d63483a0d4be4a970d6fb8", size = 1912113, upload-time = "2025-09-15T20:24:13.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl", hash = "sha256:b08233118824423b5fc239f7dd51f145e7018082b4164f8da6a9994e1f1ae793", size = 1953940, upload-time = "2025-09-15T20:24:11.482Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" }, ] [[package]] name = "fido2" -version = "2.0.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/b9/6ec8d8ec5715efc6ae39e8694bd48d57c189906f0628558f56688d0447b2/fido2-2.0.0.tar.gz", hash = "sha256:3061cd05e73b3a0ef6afc3b803d57c826aa2d6a9732d16abd7277361f58e7964", size = 274942, upload-time = "2025-05-20T09:45:00.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/34/4837e2f5640baf61d8abd6125ccb6cc60b4b2933088528356ad6e781496f/fido2-2.2.0.tar.gz", hash = "sha256:0d8122e690096ad82afde42ac9d6433a4eeffda64084f36341ea02546b181dd1", size = 294167, upload-time = "2026-04-15T06:42:50.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/7d/a1dba174d7ec4b6b8d6360eed0ac3a4a4e2aa45f234e903592d3184c6c3f/fido2-2.0.0-py3-none-any.whl", hash = "sha256:685f54a50a57e019c6156e2dd699802a603e3abf70bab334f26affdd4fb8d4f7", size = 224761, upload-time = "2025-05-20T09:44:59.029Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/f3c5dd87b0977f5547cc132b7969e6f5075a8c2f5881cf4b6df6378505f9/fido2-2.2.0-py3-none-any.whl", hash = "sha256:3587ccf0af7b71b5dd73f17e1dbec9f0fd157292f9163f02e7778f46d0d25fe5", size = 234025, upload-time = "2026-04-15T06:42:51.813Z" }, ] [[package]] @@ -656,36 +750,36 @@ wheels = [ [[package]] name = "gunicorn" -version = "23.0.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] [[package]] name = "icalendar" -version = "6.3.1" +version = "7.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/13/e5899c916dcf1343ea65823eb7278d3e1a1d679f383f6409380594b5f322/icalendar-6.3.1.tar.gz", hash = "sha256:a697ce7b678072941e519f2745704fc29d78ef92a2dc53d9108ba6a04aeba466", size = 177169, upload-time = "2025-05-20T07:42:50.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/60/6b0356a2ed1c9689ae14bd8e44f22eac67c420a0ecca4df8306b70906600/icalendar-7.0.3.tar.gz", hash = "sha256:95027ece087ab87184d765f03761f25875821f74cdd18d3b57e9c868216d8fde", size = 443788, upload-time = "2026-03-03T12:00:10.952Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/25/b5fc00e85d2dfaf5c806ac8b5f1de072fa11630c5b15b4ae5bbc228abd51/icalendar-6.3.1-py3-none-any.whl", hash = "sha256:7ea1d1b212df685353f74cdc6ec9646bf42fa557d1746ea645ce8779fdfbecdd", size = 242349, upload-time = "2025-05-20T07:42:48.589Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c6/431fbf9063a6a4306d4cedae7823d69baf0979ba6ca57ab24a9d898cd0aa/icalendar-7.0.3-py3-none-any.whl", hash = "sha256:8c9fea6d3a89671bba8b6938d8565b4d0ec465c6a2796ef0f92790dcb9e627cd", size = 442406, upload-time = "2026-03-03T12:00:09.228Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -699,29 +793,29 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "markdown" -version = "3.8.2" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -735,20 +829,20 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "parse" -version = "1.20.2" +version = "1.21.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/18/0bea374e5ec3c8ba15365570002187f3fef9d7265ffbc2f649529878cc80/parse-1.21.1.tar.gz", hash = "sha256:825e1a88e9d9fb481b8d2ca709c6195558b6eaa97c559ad3a9a20aa2d12815a3", size = 29105, upload-time = "2026-02-19T02:20:07.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/c3/13/114daf766c33aec6c5a3954e7ea653f8a7ade9602c5c5a2228281698c490/parse-1.21.1-py2.py3-none-any.whl", hash = "sha256:55339ca698019815df3b8e8b550e5933933527e623b0cdf1ca2f404da35ffb47", size = 19693, upload-time = "2026-02-19T02:20:06.575Z" }, ] [[package]] @@ -766,32 +860,60 @@ wheels = [ [[package]] name = "pillow" -version = "11.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, - { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, - { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, - { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, - { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, - { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, - { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, - { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, - { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, - { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, - { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] @@ -805,72 +927,111 @@ wheels = [ [[package]] name = "prek" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" }, - { url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" }, - { url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" }, - { url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" }, - { url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" }, - { url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" }, - { url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" }, - { url = "https://files.pythonhosted.org/packages/73/42/1bb4bba3ff47897df11e9dfd774027cdfa135482c961a54e079af0faf45a/prek-0.3.2-py3-none-win32.whl", hash = "sha256:58c806bd1344becd480ef5a5ba348846cc000af0e1fbe854fef91181a2e06461", size = 4267619, upload-time = "2026-02-06T13:49:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/97/11/6665f47a7c350d83de17403c90bbf7a762ef50876ece456a86f64f46fbfb/prek-0.3.2-py3-none-win_amd64.whl", hash = "sha256:70114b48e9eb8048b2c11b4c7715ce618529c6af71acc84dd8877871a2ef71a6", size = 4624324, upload-time = "2026-02-06T13:49:45.922Z" }, - { url = "https://files.pythonhosted.org/packages/22/e7/740997ca82574d03426f897fd88afe3fc8a7306b8c7ea342a8bc1c538488/prek-0.3.2-py3-none-win_arm64.whl", hash = "sha256:9144d176d0daa2469a25c303ef6f6fa95a8df015eb275232f5cb53551ecefef0", size = 4336008, upload-time = "2026-02-06T13:49:52.27Z" }, +version = "0.3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/31/3e68cd8f45cb5f3f81009ff42d3a3cde0887b83f214a4eb0ded9ae239cc2/prek-0.3.10.tar.gz", hash = "sha256:f4e9c533612bbaa9f89eca0e80ab6e59a05b0fd15ca7c6642a35bb303731ad6f", size = 425926, upload-time = "2026-04-21T11:29:37.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/b7/306d57aaaf503be1ee58929f9e58e812fbde9bea52c1a578e4996d82425a/prek-0.3.10-py3-none-linux_armv6l.whl", hash = "sha256:7250661f003d902b7b601141d64c7015f93eeeeeb1c485f714374582714d7c51", size = 5416913, upload-time = "2026-04-21T11:29:33.716Z" }, + { url = "https://files.pythonhosted.org/packages/93/ec/4b4b60d17c5eb639779a059d7f66a8fff5b38c7cb26c9c5b3a853cc6a9c8/prek-0.3.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9da699368bb11e85fc58b57ae4c1ccc703364b1d95b595bd17a27b27fa6df50d", size = 5784725, upload-time = "2026-04-21T11:29:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/6d4dcc9446a1630a888e3f105c4775536afe58c20f18833fec8e0d841ac0/prek-0.3.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f245fa9935cee6ea7d5ba1c6e7dd7546ccf6f01fb7d604bb6a344c8e3b49a9c2", size = 5360071, upload-time = "2026-04-21T11:29:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/36/2d/d1f63ca15f4a275fcdc712fd9ef64787abb4ba86f2e79986a26108a5f1e7/prek-0.3.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d321cad394ef4436ed9fce74150076a5eefabe310ed01b3f5735746284b9d046", size = 5615550, upload-time = "2026-04-21T11:29:30.985Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1b/a925050ba30791e50e7690a6dabe33dc08a0133e7efab4b9d91088261d40/prek-0.3.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e80017ecf06a11bf9c8ebcb67d38183e0abf2f18e012ee6a5f91d1b9c065d6", size = 5338159, upload-time = "2026-04-21T11:29:25.314Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/7d563a333198ead10f9a13fcf72ca910a59135b5d58429ed3b1fff9a0ff9/prek-0.3.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba24b4c8e40cb9ae96d58efa3eb91813cdff4f10b78b79201c93a0dc6b8763e2", size = 5728600, upload-time = "2026-04-21T11:29:23.687Z" }, + { url = "https://files.pythonhosted.org/packages/4c/88/10d493ef308f10321b92bb4e65ab60030b3c5c6304790c678be249e383e3/prek-0.3.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aac1c6f8dabb0a202a27516bf78cbfbe8a05b7cfebe2078ff6d0e12e7c77c7d5", size = 6610055, upload-time = "2026-04-21T11:29:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/c4/14/552b239d99fdd09fd6664b69089359340778f84a9f866a1fecc38b66c811/prek-0.3.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9a446e178562e2c3cfc71c8f0b21043b571209533a104042b6b9e36662849e", size = 6007394, upload-time = "2026-04-21T11:29:14.466Z" }, + { url = "https://files.pythonhosted.org/packages/21/80/12b9de6b6721a27f57ed633a8864435ec2fa7535cba7f48f3ea4a52ce683/prek-0.3.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d7344b2acb88b52592c0e0f3dd33349f75e6a4813d3bb44385aa8c70084990ca", size = 5616625, upload-time = "2026-04-21T11:29:29.511Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/7235f6b6d65a3e6e036b211a240432bc4f51adfdbbdcc03b5ce60a238a84/prek-0.3.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f2fe5ca3fe797a647243c10c0a9f9c2c27c1d4a0cdcb7f836a0cb8c5c2c603d8", size = 5431581, upload-time = "2026-04-21T11:29:20.399Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/0dd0013c6a9c6b23d070a57fe5c015c40c7b3e84217e4fb1ca9e465768c6/prek-0.3.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6d39031e4acf7670b905d77ea807e2ce1b6309604d54740f3152d601b71ae5e0", size = 5318252, upload-time = "2026-04-21T11:29:40.34Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/5407ca904a7f2ffe63b1333e503d2f21ec23eda93e93fb45a491d78e0d76/prek-0.3.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:27865f3134dd566ffd76388a9f0de6ee931336794e32741b8e22c90a61f3fc39", size = 5589312, upload-time = "2026-04-21T11:29:18.3Z" }, + { url = "https://files.pythonhosted.org/packages/e0/05/460b0c498db65e9a2e590b6918aad3395291d8d9f2c080fce7201d12a5de/prek-0.3.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3e6b65338a06add2bc47a86f837607b4705855ade9457f2f50676a7a5a509cd9", size = 6122323, upload-time = "2026-04-21T11:29:12.6Z" }, + { url = "https://files.pythonhosted.org/packages/ca/63/fd715bcd9ed5debd60dc589d6117ab9eaf0ef0b106e0bb976e1bcaa96e29/prek-0.3.10-py3-none-win32.whl", hash = "sha256:e2529515ce81292c938181702dbfc75516efa5bb1d4dc1295e6f8b2655eec881", size = 5115503, upload-time = "2026-04-21T11:29:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0d/a34f57ca3efa228fa75a976fcf698d5705b5b9a750c8dd1b86857b24f3b0/prek-0.3.10-py3-none-win_amd64.whl", hash = "sha256:0846228fa277a8bdf5a3eeea90eb3ac8eff2bf71900919373f12d142e19c8f8c", size = 5496962, upload-time = "2026-04-21T11:29:16.71Z" }, + { url = "https://files.pythonhosted.org/packages/ea/de/8b9f20f712c21756e52feed95fdacd52f48811a7bc6e3816cd04725b1240/prek-0.3.10-py3-none-win_arm64.whl", hash = "sha256:dedcdb4e5a52ee3e0463c061cabdcb94a7443875053cebfadb8228b3bf917035", size = 5340403, upload-time = "2026-04-21T11:29:22.11Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "psycopg2-binary" -version = "2.9.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, - { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, - { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, - { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, - { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, - { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, ] [[package]] name = "pycparser" -version = "2.22" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [package.optional-dependencies] @@ -880,7 +1041,7 @@ crypto = [ [[package]] name = "pytest" -version = "8.4.1" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -889,21 +1050,21 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "pytest-django" -version = "4.11.1" +version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" }, ] [[package]] @@ -922,6 +1083,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/2f/4f73a79196b4acb0f902520a805caa22f8ba0adbecdfb028a371404c2537/pytest_factoryboy-2.8.1-py3-none-any.whl", hash = "sha256:91c762cb236bf34b11efdf2e54bafae33114488235621e8b2c4bd9fd77838784", size = 16413, upload-time = "2025-07-01T04:05:37.344Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[package.optional-dependencies] +psutil = [ + { name = "psutil" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -936,20 +1115,20 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -966,7 +1145,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -974,72 +1153,59 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "ruff" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] name = "s3transfer" -version = "0.13.0" +version = "0.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/29/af14f4ef3c11a50435308660e2cc68761c9a7742475e0585cd4396b91777/s3transfer-0.16.1.tar.gz", hash = "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524", size = 154801, upload-time = "2026-04-22T20:36:06.475Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, + { url = "https://files.pythonhosted.org/packages/03/19/90d7d4ed51932c022d53f1d02d564b62d10e272692a1f9b76425c1ad2a02/s3transfer-0.16.1-py3-none-any.whl", hash = "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", size = 86825, upload-time = "2026-04-22T20:36:04.992Z" }, ] [[package]] name = "sentry-sdk" -version = "2.32.0" +version = "2.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" }, + { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, ] [package.optional-dependencies] @@ -1058,20 +1224,20 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.3" 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/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } 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/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sqlparse" -version = "0.5.3" +version = "0.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +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/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, + { 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]] @@ -1096,6 +1262,7 @@ dependencies = [ { name = "gunicorn" }, { name = "pillow" }, { name = "psycopg2-binary" }, + { name = "pytest-xdist", extra = ["psutil"] }, { name = "python-dotenv" }, { name = "sentry-sdk", extra = ["django"] }, { name = "whitenoise" }, @@ -1137,6 +1304,7 @@ requires-dist = [ { name = "gunicorn", specifier = ">=23.0.0" }, { name = "pillow", specifier = ">=11.1.0" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pytest-xdist", extras = ["psutil"], specifier = ">=3.8.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "sentry-sdk", extras = ["django"], specifier = ">=2.24.0" }, { name = "whitenoise", specifier = ">=6.9.0" }, @@ -1173,29 +1341,29 @@ wheels = [ [[package]] name = "types-psycopg2" -version = "2.9.21.20250516" +version = "2.9.21.20260422" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/55/3f94eff9d1a1402f39e19523a90117fe6c97d7fc61957e7ee3e3052c75e1/types_psycopg2-2.9.21.20250516.tar.gz", hash = "sha256:6721018279175cce10b9582202e2a2b4a0da667857ccf82a97691bdb5ecd610f", size = 26514, upload-time = "2025-05-16T03:07:45.786Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a2/ecb04604074a7f2e82231ab1f2d3b5a792589aa3c21a597cb3232a38ece3/types_psycopg2-2.9.21.20260422.tar.gz", hash = "sha256:ad7574fa8e25d9aa96ab96cd280c4dee20872725cd1fe6a6d3facc354f2644d4", size = 27123, upload-time = "2026-04-22T04:36:33.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/50/f5d74945ab09b9a3e966ad39027ac55998f917eca72ede7929eab962b5db/types_psycopg2-2.9.21.20250516-py3-none-any.whl", hash = "sha256:2a9212d1e5e507017b31486ce8147634d06b85d652769d7a2d91d53cb4edbd41", size = 24846, upload-time = "2025-05-16T03:07:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/82f86c2d0a7ae4d335c6fe3c4ad193c4a57f0d6bfe1a676289cf63667275/types_psycopg2-2.9.21.20260422-py3-none-any.whl", hash = "sha256:e240684ac37946c5a2a058b04ea1f2fd0e4ee2655719b8c3ec9abf37f96da5ba", size = 24918, upload-time = "2026-04-22T04:36:32.108Z" }, ] [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "tzdata" -version = "2025.2" +version = "2026.2" 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/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } 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/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] @@ -1212,11 +1380,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -1230,9 +1398,9 @@ wheels = [ [[package]] name = "whitenoise" -version = "6.9.0" +version = "6.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/cf/c15c2f21aee6b22a9f6fc9be3f7e477e2442ec22848273db7f4eb73d6162/whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", size = 25920, upload-time = "2025-02-06T22:16:34.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b2/2ce9263149fbde9701d352bda24ea1362c154e196d2fda2201f18fc585d7/whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df", size = 20161, upload-time = "2025-02-06T22:16:32.589Z" }, + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, ] From 5d6bd5e6b4639ede050066638573c8274974cb74 Mon Sep 17 00:00:00 2001 From: David Reed Date: Wed, 4 Feb 2026 14:04:41 -0700 Subject: [PATCH 24/26] Testing --- stave/avail.py | 1 + tests/test_avail.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index a462550..6fcdecb 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -581,6 +581,7 @@ def set_assignment( else: raise UserNotAvailableException("There is no application for user") + # FIXME: old_availability_entries is empty old_availability_entries = self.get_swappable_assignments( user, crew, crew.get_context(), role ) diff --git a/tests/test_avail.py b/tests/test_avail.py index cb9a34d..9aa347a 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -1,6 +1,6 @@ from zeal import zeal_ignore -from tests.factories import ApplicationFactory, CrewFactory, ApplicationFormFactory +from tests.factories import ApplicationFactory, CrewFactory, ApplicationFormFactory, RoleFactory from datetime import datetime, timedelta, timezone from stave.avail import AvailabilityManager, UserAvailabilityEntry, ConflictKind from pytest import fixture @@ -486,14 +486,52 @@ def test_set_assignment__remove_existing(db): am = AvailabilityManager.with_application_form(application.form) am.set_assignment(role, crew, None) - assert role.id not in crew.get_assignments_by_role_id() assert not models.CrewAssignment.objects.filter( user=application.user ).exists() application.refresh_from_db() assert application.status == models.ApplicationStatus.APPLIED + assert crew.get_assignments_by_role_id()[role.id].user is None -def test_set_assignment__swap_roles_override_crew(db):... +def test_set_assignment__swap_roles_override_crew(db): + application = ApplicationFactory( + status=models.ApplicationStatus.ASSIGNMENT_PENDING, + form__application_kind=models.ApplicationKind.ASSIGN_ONLY + ) + role = application.roles.first() + other_role = RoleFactory(role_group=role.role_group) + other_application = ApplicationFactory( + form=application.form, + status=models.ApplicationStatus.ASSIGNMENT_PENDING + ) + other_application.roles.set([role, other_role]) + crew = CrewFactory( + event=application.form.event, + role_group=role.role_group + ) + models.CrewAssignment.objects.create( + user=application.user, + crew=crew, + role=role + ) + models.CrewAssignment.objects.create( + user=other_application.user, + crew=crew, + role=other_role + ) + + am = AvailabilityManager.with_application_form(application.form) + am.set_assignment(role, crew, other_application.user) + + assert not models.CrewAssignment.objects.filter( + user=application.user + ).exists() + application.refresh_from_db() + assert application.status == models.ApplicationStatus.APPLIED + other_application.refresh_from_db() + assert other_application.status == models.ApplicationStatus.ASSIGNMENT_PENDING + assert crew.get_assignments_by_role_id()[role.id].user == other_application.user + assert crew.get_assignments_by_role_id()[other_role.id].user is None def test_set_assignment__swap_roles_static_crew_to_override_crew(db): ... From c365d9caf49c9abcc1485a32b4b11e824e568766 Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 5 Feb 2026 18:01:13 -0700 Subject: [PATCH 25/26] Testing --- stave/avail.py | 1 + tests/test_avail.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/stave/avail.py b/stave/avail.py index 6fcdecb..9706b9e 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -582,6 +582,7 @@ def set_assignment( raise UserNotAvailableException("There is no application for user") # FIXME: old_availability_entries is empty + # And it's because crew.get_context() is None old_availability_entries = self.get_swappable_assignments( user, crew, crew.get_context(), role ) diff --git a/tests/test_avail.py b/tests/test_avail.py index 9aa347a..aa2f3c7 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -1,6 +1,6 @@ from zeal import zeal_ignore -from tests.factories import ApplicationFactory, CrewFactory, ApplicationFormFactory, RoleFactory +from tests.factories import ApplicationFactory, CrewFactory, ApplicationFormFactory, RoleFactory, GameFactory from datetime import datetime, timedelta, timezone from stave.avail import AvailabilityManager, UserAvailabilityEntry, ConflictKind from pytest import fixture @@ -506,9 +506,18 @@ def test_set_assignment__swap_roles_override_crew(db): ) other_application.roles.set([role, other_role]) crew = CrewFactory( + kind=models.CrewKind.OVERRIDE_CREW, event=application.form.event, role_group=role.role_group ) + # Swapping roles requires the Crew have a non-None + # get_context() + game = GameFactory(event=application.form.event) + models.RoleGroupCrewAssignment.objects.create( + crew_overrides=crew, + game=game, + role_group=role.role_group + ) models.CrewAssignment.objects.create( user=application.user, crew=crew, From 5c55239cb5ed1bfd15520cab242455d176efb344 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sun, 10 May 2026 11:01:19 -0600 Subject: [PATCH 26/26] Merge migrations --- stave/migrations/0064_merge_20260510_1649.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 stave/migrations/0064_merge_20260510_1649.py diff --git a/stave/migrations/0064_merge_20260510_1649.py b/stave/migrations/0064_merge_20260510_1649.py new file mode 100644 index 0000000..afb7be2 --- /dev/null +++ b/stave/migrations/0064_merge_20260510_1649.py @@ -0,0 +1,12 @@ +# Generated by Django 5.2.13 on 2026-05-10 16:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("stave", "0059_merge_20260203_0256"), + ("stave", "0063_message_email_alter_message_user"), + ] + + operations = []
{% csrf_token %} - + - View + View
{{ application.get_status_display }}{{ game_count }}{{ entry.application.get_status_display }}{{ entry.user_game_count }} @@ -29,10 +29,10 @@ - {% for role in application.roles.all %} + {% for role in entry.application.roles.all %} {% if role.role_group_id == role_group.id %} {{ role }} {% endif %} @@ -93,7 +93,7 @@ {% endif %} {# Application questions #} {% for question in form.form_questions.all %} - {% with response=application.responses_by_question|get:question.id %} + {% with entry.application.responses_by_question|get:question.id as response %} {% include "stave/partials/abbrev.html" with text=response only %} diff --git a/stave/templates/stave/partials/crew_editor.html b/stave/templates/stave/partials/crew_editor.html index 9e40b7e..cdf8785 100644 --- a/stave/templates/stave/partials/crew_editor.html +++ b/stave/templates/stave/partials/crew_editor.html @@ -40,7 +40,9 @@ + {% if assignment %} + {% endif %} {% csrf_token %} {% endwith %} diff --git a/stave/views.py b/stave/views.py index 5a2de04..a072fe8 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2013,6 +2013,7 @@ def get( ) + class CrewBuilderDetailView(LoginRequiredMixin, views.View): """A view rendering the Crew Builder with a list of applications for a given position. On GET, renders the view. @@ -2053,15 +2054,7 @@ def get( game = crew.get_context() else: game = None - applications = am.get_available_applications(crew, game, role) - all_applications = am.get_all_applications(crew, game, role) - unavail_applications = [ - a for a in all_applications if a not in applications - ] # TODO: efficiency! - - game_counts = { - a.user.id: am.get_game_count_for_user(a.user) for a in all_applications - } + applications = am.get_application_entries(crew, game, role) # TODO: get the Game from AM to reduce queries. return render( @@ -2071,11 +2064,9 @@ def get( contexts.CrewBuilderDetailInputs( form=application_form, applications=applications, - unavail_applications=unavail_applications, game=game, event=am.application_form.event, role=role, - game_counts=game_counts, ) ), ) @@ -2161,21 +2152,34 @@ def post( # an exclusive role, AND that assignment is within # one of this form's Role Groups, clear that assignment. - # We need to assess any existing assignments for this user. - # If there are nonconflicting assignments, we'll ignore them. - # If there are "swappable" assignments, we'll delete them. - # If there are "non-swappable" assignments, we'll return an error. - - # An assignment is nonconflicting if it and this assignment are in - # the same role group and one of them is nonexclusive. - # An assignment is swappable if it is in one of the other role groups - # managed by this form (including this role group) and is conflicting. - # TODO: display current assignment on crew builder detail # TODO: show users who are time-available but not role-available - old_assignments = am.get_swappable_assignments(crew, crew.get_context(), role) - old_assignments.delete() + old_availability_entries = am.get_swappable_assignments(applications[0].user, crew, crew.get_context(), role) + for avail_entry in old_availability_entries: + # This UserAvailabilityEntry might come from a direct assignment + # or from a static crew assignment; there are different ways + # to override those. + match avail_entry.crew.kind: + case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: + # Add an overriding CrewAssignment to blank for each assignment + # of this user in the crew. + # FIXME: ensure that we do not allow this CrewAssignment to be deleted + for ca in models.CrewAssignment.objects.filter( + crew = avail_entry.crew, + user=applications[0].user + ): + models.CrewAssignment.objects.create( + role=ca.role, + crew=crew, + user=None + ) + case models.CrewKind.OVERRIDE_CREW: + # Just query for and delete the CrewAssignment + models.CrewAssignment.objects.filter( + user=applications[0].user, + + ) # Finally, add the new assignment. From 16167ebc65114bfb2b4e217636822f8ebc3305ae Mon Sep 17 00:00:00 2001 From: David Reed Date: Fri, 28 Nov 2025 15:13:20 -0700 Subject: [PATCH 08/26] WIP --- stave/avail.py | 2 ++ stave/templates/stave/contexts.py | 1 + .../templates/stave/crew_builder_detail.html | 20 ++++++++++--------- stave/views.py | 6 +++++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 4e37d28..5477d42 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -15,6 +15,8 @@ class ConflictKind(enum.Enum): NON_SWAPPABLE_CONFLICT = 2 SWAPPABLE_CONFLICT = 3 +ConflictKind.do_not_call_in_templates = True + @dataclass class ApplicationEntry: application: models.Application diff --git a/stave/templates/stave/contexts.py b/stave/templates/stave/contexts.py index 242afde..0da25da 100644 --- a/stave/templates/stave/contexts.py +++ b/stave/templates/stave/contexts.py @@ -51,6 +51,7 @@ class CrewBuilderDetailInputs: role: models.Role game: models.Game | None applications: list[avail.ApplicationEntry] + ConflictKind: type = avail.ConflictKind @dataclass diff --git a/stave/templates/stave/crew_builder_detail.html b/stave/templates/stave/crew_builder_detail.html index ee244ba..8ad56be 100644 --- a/stave/templates/stave/crew_builder_detail.html +++ b/stave/templates/stave/crew_builder_detail.html @@ -15,18 +15,24 @@

{% include 'stave/partials/application_table_header.html' with form=form only %}

{% csrf_token %} + {% if entry.availability_status == ConflictKind.NONE %} - View + {% else %} + + {% endif %} + View
No applicants are available. @@ -42,19 +48,15 @@

{% include 'stave/partials/application_table_header.html' with form=form only %} - {% for entry in unavail_applications %} + {% for entry in applications %} + {% if entry.availability_status == ConflictStatus.NON_SWAPPABLE_CONFLICT %} {% include 'stave/partials/application_table_row.html' with form=form entry=entry only %} + {% endif %} {% endfor %}
-
- {% csrf_token %} - - - - View -
+ View
diff --git a/stave/views.py b/stave/views.py index a072fe8..f1e7690 100644 --- a/stave/views.py +++ b/stave/views.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +import logging from collections import defaultdict import csv from dataclasses import is_dataclass @@ -38,7 +39,7 @@ from stave.templates.stave import contexts from . import forms, models, settings -from .avail import AvailabilityManager, ScheduleManager +from .avail import AvailabilityManager, ScheduleManager, ConflictKind if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -1893,6 +1894,7 @@ def get( for crew in sm.event.crews.all(): for role in crew.role_group.roles.all(): counts[crew.role_group.id][crew.id][role.name] = (0, 0) + # FIXME: ?? return render( request, @@ -2056,6 +2058,7 @@ def get( game = None applications = am.get_application_entries(crew, game, role) + print(f"{ConflictKind}, {applications}") # TODO: get the Game from AM to reduce queries. return render( request, @@ -2067,6 +2070,7 @@ def get( game=game, event=am.application_form.event, role=role, + ConflictKind=ConflictKind ) ), ) From a7073de37dad2f49c17a534ca08d091d3d0cb715 Mon Sep 17 00:00:00 2001 From: David Reed Date: Fri, 28 Nov 2025 21:56:20 -0700 Subject: [PATCH 09/26] WIP --- stave/avail.py | 35 ++++++++++++------- .../templates/stave/partials/crew_editor.html | 12 +++++-- stave/views.py | 20 ++++++++--- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 5477d42..36ef47b 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -33,30 +33,41 @@ class UserAvailabilityEntry: exclusive: bool def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[models.RoleGroup]) -> ConflictKind: - if self.crew.kind != other.crew.kind: - return ConflictKind.NONE - - if self.crew.id == other.crew.id: + if self.crew.role_group_id == other.crew.role_group_id: if self.exclusive and other.exclusive: return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NONE - match self.crew.kind: - case models.CrewKind.OVERRIDE_CREW: + has_times = None not in (self.start_time, self.end_time, other.start_time, other.end_time) + + match (self.crew.kind, other.crew.kind, has_times): + case (models.CrewKind.OVERRIDE_CREW, models.CrewKind.OVERRIDE_CREW, _) | (models.CrewKind.GAME_CREW, models.CrewKind.OVERRIDE_CREW, True): + # This is a comparison between two override crews, which always + # have defined start and end times, or between two assigned static crews + # or an assigned static crew and an override crew, which will + # also have times. time_overlap = (self.start_time < other.end_time) and ( self.end_time > other.start_time ) if time_overlap: - if self.crew.role_group in swappable_role_groups: + if other.crew.role_group in swappable_role_groups: return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NON_SWAPPABLE_CONFLICT - case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: - return ConflictKind.SWAPPABLE_CONFLICT - - return ConflictKind.NONE + case ( + (models.CrewKind.EVENT_CREW, models.CrewKind.EVENT_CREW, False) + | (models.CrewKind.GAME_CREW, models.CrewKind.GAME_CREW, False) + ): + # This comparison is between static crews _without_ assignments + # or between event crews. No times involved. + if other.crew.role_group in swappable_role_groups: + return ConflictKind.SWAPPABLE_CONFLICT + else: + return ConflictKind.NON_SWAPPABLE_CONFLICT + case _: + return ConflictKind.NONE class ScheduleManager: @@ -284,7 +295,7 @@ def user_availability(self) -> dict[UUID, list[UserAvailabilityEntry]]: if assignment.user_id: user_assigned_times_map[assignment.user_id].append( UserAvailabilityEntry( - crew=rgca.crew_overrides, + crew=assignment.crew, start_time=game.start_time, end_time=game.end_time, exclusive=not assignment.role.nonexclusive, diff --git a/stave/templates/stave/partials/crew_editor.html b/stave/templates/stave/partials/crew_editor.html index cdf8785..3bd89ff 100644 --- a/stave/templates/stave/partials/crew_editor.html +++ b/stave/templates/stave/partials/crew_editor.html @@ -36,11 +36,19 @@
- {% if assignment %}{% if assignment.crew.id == crew.id %}🔄{% else %}⬇️{% endif %}{% else %}🔍{% endif %} + {% if assignment and assignment.user %} + {% if assignment.crew.id == crew.id %} + 🔄 + {% else %} + ⬇️ + {% endif %} + {% else %} + 🔍 + {% endif %} - {% if assignment %} + {% if assignment and assignment.user %} {% endif %} {% csrf_token %} diff --git a/stave/views.py b/stave/views.py index f1e7690..61c1999 100644 --- a/stave/views.py +++ b/stave/views.py @@ -2058,7 +2058,6 @@ def get( game = None applications = am.get_application_entries(crew, game, role) - print(f"{ConflictKind}, {applications}") # TODO: get the Game from AM to reduce queries. return render( request, @@ -2150,6 +2149,8 @@ def post( user=None ) + breakpoint() + # Add a new assignment, if requested if applications: # If the newly-selected user is already assigned to @@ -2160,15 +2161,19 @@ def post( # TODO: show users who are time-available but not role-available old_availability_entries = am.get_swappable_assignments(applications[0].user, crew, crew.get_context(), role) + print(f"Entries: {old_availability_entries}") for avail_entry in old_availability_entries: # This UserAvailabilityEntry might come from a direct assignment # or from a static crew assignment; there are different ways # to override those. + print(f"Crew is {avail_entry.crew}, context {avail_entry.crew.get_context()}, {avail_entry.crew.kind}") + # FIXME: problem is that all Availability Entries are clamped to show as part of override crew match avail_entry.crew.kind: case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: # Add an overriding CrewAssignment to blank for each assignment # of this user in the crew. # FIXME: ensure that we do not allow this CrewAssignment to be deleted + # FIXME: ensure that we handle nonexclusive roles for ca in models.CrewAssignment.objects.filter( crew = avail_entry.crew, user=applications[0].user @@ -2179,11 +2184,18 @@ def post( user=None ) case models.CrewKind.OVERRIDE_CREW: - # Just query for and delete the CrewAssignment - models.CrewAssignment.objects.filter( + # Just query for and delete the relevant CrewAssignments + # If we're reassigning within the same crew, + # just remove exclusive roles; otherwise, all roles. + cas = models.CrewAssignment.objects.filter( user=applications[0].user, - + crew=avail_entry.crew ) + if crew == avail_entry.crew: + cas = cas.filter(role__nonexclusive=False) + + print(f"About to delete {cas}") + cas.delete() # Finally, add the new assignment. From b471a71c4b50f636be05ad948ec661e5e6c8fee3 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 29 Nov 2025 16:54:12 -0700 Subject: [PATCH 10/26] WIP --- stave/avail.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stave/avail.py b/stave/avail.py index 36ef47b..6447bfa 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -69,6 +69,10 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo case _: return ConflictKind.NONE +# FIXME: when we remove a static crew assignment (RGCA), also remove all null-user overrides. +# FIXME: when we do an override to none on a static crew ,then change it to user, I believe the override-to-none is sticking around. +# This might be an issue where removing an override needs to add an override-to-none to prevent a conflict +# FIXME: doing a swap out from a static crew slot to the same crew removes nonexclusive roles class ScheduleManager: event: models.Event From f72bb14291b88777938da56b96163fa804a0d545 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 29 Nov 2025 19:55:45 -0700 Subject: [PATCH 11/26] WIP --- stave/avail.py | 5 -- .../templates/stave/partials/crew_editor.html | 4 +- stave/views.py | 84 +++++++++++-------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 6447bfa..4f6fafa 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -69,11 +69,6 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo case _: return ConflictKind.NONE -# FIXME: when we remove a static crew assignment (RGCA), also remove all null-user overrides. -# FIXME: when we do an override to none on a static crew ,then change it to user, I believe the override-to-none is sticking around. -# This might be an issue where removing an override needs to add an override-to-none to prevent a conflict -# FIXME: doing a swap out from a static crew slot to the same crew removes nonexclusive roles - class ScheduleManager: event: models.Event diff --git a/stave/templates/stave/partials/crew_editor.html b/stave/templates/stave/partials/crew_editor.html index 3bd89ff..703c0a7 100644 --- a/stave/templates/stave/partials/crew_editor.html +++ b/stave/templates/stave/partials/crew_editor.html @@ -15,8 +15,8 @@

{{ role.name }} {% if assignment and assignment.user %} {{ assignment.user.preferred_name }} diff --git a/stave/views.py b/stave/views.py index 61c1999..e8dc421 100644 --- a/stave/views.py +++ b/stave/views.py @@ -1760,25 +1760,36 @@ def post( role_group_id: UUID, crew_id: UUID | None = None, ) -> HttpResponse: - game: models.Game = get_object_or_404( - models.Game.objects.manageable(request.user), - pk=game_id, - ) - rgca: models.RoleGroupCrewAssignment = get_object_or_404( - game.role_group_crew_assignments.all(), role_group_id=role_group_id - ) - if crew_id: - crew = get_object_or_404( - models.Crew.objects.filter( - event=game.event, kind=models.CrewKind.GAME_CREW - ), - pk=crew_id, + with transaction.atomic(): + game: models.Game = get_object_or_404( + models.Game.objects.manageable(request.user), + pk=game_id, ) - else: - crew = None + rgca: models.RoleGroupCrewAssignment = get_object_or_404( + game.role_group_crew_assignments.all(), role_group_id=role_group_id + ) + if crew_id: + crew = get_object_or_404( + models.Crew.objects.filter( + event=game.event, kind=models.CrewKind.GAME_CREW + ), + pk=crew_id, + ) + else: + crew = None - rgca.crew = crew - rgca.save() + rgca.crew = crew + rgca.save() + + if not rgca.crew: + # Remove any CrewAssignments we used to + # set a static crew member to a blank. + models.CrewAssignment.objects.filter( + user=None, + crew__kind=models.CrewKind.OVERRIDE_CREW, + crew__role_group_override_assignments__game=game, + crew__role_group_id=role_group_id + ).delete() redirect_url = request.POST.get("redirect_url") if redirect_url and url_has_allowed_host_and_scheme( @@ -1894,7 +1905,6 @@ def get( for crew in sm.event.crews.all(): for role in crew.role_group.roles.all(): counts[crew.role_group.id][crew.id][role.name] = (0, 0) - # FIXME: ?? return render( request, @@ -2114,7 +2124,7 @@ def post( applications = [] # Delete existing assignment in the target role, if present - # `crew` is an override crew here. (RIGHT?? FIXME) + # `crew` is an override crew here. if assignment := models.CrewAssignment.objects.filter( role=role, crew=crew, @@ -2130,17 +2140,20 @@ def post( .exclude(status=models.ApplicationStatus.WITHDRAWN) .first() ) - # There should be exactly one. + # There should be one or zero. if existing_application: existing_application.move_status_backwards_for_unassignment() + # FIXME: if we have a static crew assigned and the underlying user + # is not available, add a None override. + # If we are removing one user from a static-crew assignment, add a blank assignment. rgca = models.RoleGroupCrewAssignment.objects.filter( role_group=role.role_group, - game=crew.get_context() # I think this is right? + game=crew.get_context() ).first() - if rgca and rgca.crew and not applications: + if rgca and rgca.crew and not assignment and not applications: # rgca.crew is a static crew. # Override with a blank in the override crew. models.CrewAssignment.objects.create( @@ -2149,9 +2162,6 @@ def post( user=None ) - breakpoint() - - # Add a new assignment, if requested if applications: # If the newly-selected user is already assigned to # an exclusive role, AND that assignment is within @@ -2161,23 +2171,30 @@ def post( # TODO: show users who are time-available but not role-available old_availability_entries = am.get_swappable_assignments(applications[0].user, crew, crew.get_context(), role) - print(f"Entries: {old_availability_entries}") for avail_entry in old_availability_entries: # This UserAvailabilityEntry might come from a direct assignment # or from a static crew assignment; there are different ways # to override those. - print(f"Crew is {avail_entry.crew}, context {avail_entry.crew.get_context()}, {avail_entry.crew.kind}") - # FIXME: problem is that all Availability Entries are clamped to show as part of override crew match avail_entry.crew.kind: case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: # Add an overriding CrewAssignment to blank for each assignment - # of this user in the crew. + # of this user in the crew, unless we're assigning to the same + # game, in which case we keep nonexclusive assignments. # FIXME: ensure that we do not allow this CrewAssignment to be deleted - # FIXME: ensure that we handle nonexclusive roles - for ca in models.CrewAssignment.objects.filter( + keep_nonexclusive = ( + # Swap within a crew + avail_entry.crew == crew + # Swap from a static crew to an override crew in the same context + or avail_entry.crew.get_context() == crew.get_context() + ) + cas = models.CrewAssignment.objects.filter( crew = avail_entry.crew, user=applications[0].user - ): + ) + if keep_nonexclusive: + cas = cas.exclude(role__nonexclusive=True) + + for ca in cas: models.CrewAssignment.objects.create( role=ca.role, crew=crew, @@ -2192,9 +2209,8 @@ def post( crew=avail_entry.crew ) if crew == avail_entry.crew: - cas = cas.filter(role__nonexclusive=False) + cas = cas.exclude(role__nonexclusive=True) - print(f"About to delete {cas}") cas.delete() # Finally, add the new assignment. From bf4752936c6de798c9d86912a9b0119bf0aa5760 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 29 Nov 2025 20:16:34 -0700 Subject: [PATCH 12/26] WIP --- stave/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stave/views.py b/stave/views.py index e8dc421..290ebd8 100644 --- a/stave/views.py +++ b/stave/views.py @@ -1761,6 +1761,7 @@ def post( crew_id: UUID | None = None, ) -> HttpResponse: with transaction.atomic(): + # FIXME: check availability for all crew members. game: models.Game = get_object_or_404( models.Game.objects.manageable(request.user), pk=game_id, From 276ccdc146cc66615a1250d148ffbf7b2be0db1f Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 29 Nov 2025 21:27:43 -0700 Subject: [PATCH 13/26] Add tests --- stave/management/commands/seed.py | 2 +- tests/test_avail.py | 109 +++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/stave/management/commands/seed.py b/stave/management/commands/seed.py index 0387df2..adf3ac5 100644 --- a/stave/management/commands/seed.py +++ b/stave/management/commands/seed.py @@ -521,7 +521,7 @@ def create_tournament_app( ) app.availability_by_game.set(doubleheader.games.all()) - app.roles.set([role_hnso, role_jt, role_plt, role_pbm]) + app.roles.set([role_hnso, role_jt, role_plt, role_pbm, role_hr]) app = models.Application.objects.create( form=doubleheader_app_form, user=drummer, diff --git a/tests/test_avail.py b/tests/test_avail.py index c7a9f45..c997e8d 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -1,9 +1,114 @@ from zeal import zeal_ignore -from tests.factories import ApplicationFactory +from tests.factories import ApplicationFactory, CrewFactory +from datetime import datetime, timedelta, timezone +from stave.avail import AvailabilityManager, UserAvailabilityEntry, ConflictKind +from pytest import fixture +from stave import models -from stave.avail import AvailabilityManager +@fixture +def existing_entry(db): + start_time = datetime.now(tz=timezone.utc) + end_time = start_time + timedelta(hours=2) + return UserAvailabilityEntry( + crew = CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), + start_time=start_time, + end_time=end_time, + exclusive=True + ) +@fixture +def existing_static_crew_entry(db): + start_time = datetime.now(tz=timezone.utc) + end_time = start_time + timedelta(hours=2) + return UserAvailabilityEntry( + crew = CrewFactory(kind=models.CrewKind.GAME_CREW), + start_time=start_time, + end_time=end_time, + exclusive=True + ) +def test_user_availability_entry__simple_overlap(existing_entry): + assert existing_entry.overlaps(UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=True + ), set()) == ConflictKind.SWAPPABLE_CONFLICT + +def test_user_availability_entry__simple_overlap_nonexclusive(existing_entry): + assert existing_entry.overlaps(UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=False + ), set()) == ConflictKind.NONE + +def test_user_availability_entry__simple_overlap_other_role_group(existing_entry): + assert existing_entry.overlaps(UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=True + ), set()) == ConflictKind.NON_SWAPPABLE_CONFLICT + +def test_user_availability_entry__swappable_overlap_other_role_group(existing_entry): + other_crew = CrewFactory(kind=models.CrewKind.OVERRIDE_CREW) + assert existing_entry.overlaps(UserAvailabilityEntry( + crew=other_crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=False + ),set([other_crew.role_group])) == ConflictKind.SWAPPABLE_CONFLICT + +def test_user_availability_entry__partial_overlap(existing_entry): + assert existing_entry.overlaps(UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time + timedelta(hours=1), + end_time=existing_entry.end_time, + exclusive=True + ), set()) == ConflictKind.SWAPPABLE_CONFLICT + +def test_user_availability_entry__event_crews(): + ... + +def test_user_availability_entry__static_crews(existing_static_crew_entry): + assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( + crew=existing_static_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=True + ), set()) == ConflictKind.SWAPPABLE_CONFLICT + +def test_user_availability_entry__static_crews_nonexclusive(existing_static_crew_entry): + assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( + crew=existing_static_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=False + ), set()) == ConflictKind.NONE + +def test_user_availability_entry__static_crews_other_role_group(existing_static_crew_entry): + assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.GAME_CREW), + start_time=None, + end_time=None, + exclusive=True + ), set()) == ConflictKind.NON_SWAPPABLE_CONFLICT + +def test_user_availability_entry__static_crews_swappable_role_group(existing_static_crew_entry): + other_crew = CrewFactory(kind=models.CrewKind.GAME_CREW) + assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.GAME_CREW), + start_time=None, + end_time=None, + exclusive=True + ), set([other_crew.role_group])) == ConflictKind.SWAPPABLE_CONFLICT + +def test_user_availability_entry__override_of_static_crew(): + ... + +def test_user_availability_entry__non_meaningful(): + ... def test_availability_manager__applications(tournament): form = tournament.application_forms.get(slug="apply-nso-so") From bed0d1a93024b4861ca7d734dcb9cc3bd3c3a43e Mon Sep 17 00:00:00 2001 From: David Reed Date: Sat, 29 Nov 2025 21:28:22 -0700 Subject: [PATCH 14/26] Format --- stave/avail.py | 88 +++++++++---- stave/models.py | 5 +- stave/templates/stave/contexts.py | 1 + stave/views.py | 30 ++--- tests/test_avail.py | 205 ++++++++++++++++++++---------- 5 files changed, 216 insertions(+), 113 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 4f6fafa..d6b4b1d 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -10,19 +10,23 @@ from . import models + class ConflictKind(enum.Enum): NONE = 1 NON_SWAPPABLE_CONFLICT = 2 SWAPPABLE_CONFLICT = 3 + ConflictKind.do_not_call_in_templates = True + @dataclass class ApplicationEntry: application: models.Application user_game_count: int availability_status: ConflictKind + @dataclass class UserAvailabilityEntry: crew: models.Crew @@ -32,17 +36,30 @@ class UserAvailabilityEntry: end_time: datetime | None exclusive: bool - def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[models.RoleGroup]) -> ConflictKind: + def overlaps( + self, + other: "UserAvailabilityEntry", + swappable_role_groups: set[models.RoleGroup], + ) -> ConflictKind: if self.crew.role_group_id == other.crew.role_group_id: if self.exclusive and other.exclusive: return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NONE - has_times = None not in (self.start_time, self.end_time, other.start_time, other.end_time) + has_times = None not in ( + self.start_time, + self.end_time, + other.start_time, + other.end_time, + ) match (self.crew.kind, other.crew.kind, has_times): - case (models.CrewKind.OVERRIDE_CREW, models.CrewKind.OVERRIDE_CREW, _) | (models.CrewKind.GAME_CREW, models.CrewKind.OVERRIDE_CREW, True): + case (models.CrewKind.OVERRIDE_CREW, models.CrewKind.OVERRIDE_CREW, _) | ( + models.CrewKind.GAME_CREW, + models.CrewKind.OVERRIDE_CREW, + True, + ): # This is a comparison between two override crews, which always # have defined start and end times, or between two assigned static crews # or an assigned static crew and an override crew, which will @@ -56,9 +73,10 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo else: return ConflictKind.NON_SWAPPABLE_CONFLICT - case ( - (models.CrewKind.EVENT_CREW, models.CrewKind.EVENT_CREW, False) - | (models.CrewKind.GAME_CREW, models.CrewKind.GAME_CREW, False) + case (models.CrewKind.EVENT_CREW, models.CrewKind.EVENT_CREW, False) | ( + models.CrewKind.GAME_CREW, + models.CrewKind.GAME_CREW, + False, ): # This comparison is between static crews _without_ assignments # or between event crews. No times involved. @@ -69,6 +87,7 @@ def overlaps(self, other: "UserAvailabilityEntry", swappable_role_groups: set[mo case _: return ConflictKind.NONE + class ScheduleManager: event: models.Event @@ -319,8 +338,7 @@ def get_game_count_for_user(self, user: models.User) -> int: @functools.cache def game_counts_by_user(self) -> dict[UUID, int]: return { - a.user.id: self.get_game_count_for_user(a.user) - for a in self.applications + a.user.id: self.get_game_count_for_user(a.user) for a in self.applications } @property @@ -361,16 +379,20 @@ def user_static_crew_availability(self) -> dict[UUID, list[UserAvailabilityEntry @functools.cache def user_game_counts(self) -> dict[UUID, int]: return { - a.user.id: self.get_game_count_for_user(a.user) - for a in all_applications + a.user.id: self.get_game_count_for_user(a.user) for a in all_applications } - def get_application_counts( self, crew: models.Crew, game: models.Game | None, role: models.Role ) -> tuple[int, int]: return ( - len([a for a in self.get_application_entries(crew, game, role) if a.availability_status is ConflictKind.NONE]), + len( + [ + a + for a in self.get_application_entries(crew, game, role) + if a.availability_status is ConflictKind.NONE + ] + ), len(self.get_all_applications(crew, game, role)), ) @@ -383,7 +405,9 @@ def get_all_applications( ) @functools.cache - def get_application_entries(self, crew: models.Crew, game: models.Game | None, role: models.Role) -> list[ApplicationEntry]: + def get_application_entries( + self, crew: models.Crew, game: models.Game | None, role: models.Role + ) -> list[ApplicationEntry]: apps = self.get_all_applications(crew, game, role) avail = self._get_avail_data_for_crew_kind(crew.kind) entries = [] @@ -402,22 +426,38 @@ def get_application_entries(self, crew: models.Crew, game: models.Game | None, r ) if ConflictKind.NON_SWAPPABLE_CONFLICT in overlaps: entries.append( - ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.NON_SWAPPABLE_CONFLICT) + ApplicationEntry( + application=app, + user_game_count=game_count, + availability_status=ConflictKind.NON_SWAPPABLE_CONFLICT, + ) ) elif ConflictKind.SWAPPABLE_CONFLICT in overlaps: entries.append( - ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.SWAPPABLE_CONFLICT) + ApplicationEntry( + application=app, + user_game_count=game_count, + availability_status=ConflictKind.SWAPPABLE_CONFLICT, + ) ) else: entries.append( - ApplicationEntry(application=app, user_game_count=game_count, availability_status=ConflictKind.NONE) + ApplicationEntry( + application=app, + user_game_count=game_count, + availability_status=ConflictKind.NONE, + ) ) return entries @functools.cache def get_swappable_assignments( - self, user: models.User, crew: models.Crew, game: models.Game | None, role: models.Role + self, + user: models.User, + crew: models.Crew, + game: models.Game | None, + role: models.Role, ) -> list[UserAvailabilityEntry]: avail = self._get_avail_data_for_crew_kind(crew.kind) @@ -428,14 +468,17 @@ def get_swappable_assignments( not role.nonexclusive, ) - return [t for t in self._get_avail_data_for_crew_kind(crew.kind)[user.id] - if t.overlaps(entry, self.role_groups) is ConflictKind.SWAPPABLE_CONFLICT - ] - + return [ + t + for t in self._get_avail_data_for_crew_kind(crew.kind)[user.id] + if t.overlaps(entry, self.role_groups) is ConflictKind.SWAPPABLE_CONFLICT + ] # Filter methods MUST NOT hit the database - use only cached data - def _get_avail_data_for_crew_kind(self, kind: models.CrewKind) -> dict[UUID, list[UserAvailabilityEntry]]: + def _get_avail_data_for_crew_kind( + self, kind: models.CrewKind + ) -> dict[UUID, list[UserAvailabilityEntry]]: match kind: case models.CrewKind.OVERRIDE_CREW: return self.user_availability @@ -444,7 +487,6 @@ def _get_avail_data_for_crew_kind(self, kind: models.CrewKind) -> dict[UUID, lis case models.CrewKind.GAME_CREW: return self.user_static_crew_availability - def _filter_for_basic_availability( self, applications: Iterable[models.Application], diff --git a/stave/models.py b/stave/models.py index 6bf6260..d0d631f 100644 --- a/stave/models.py +++ b/stave/models.py @@ -1978,10 +1978,7 @@ def move_status_backwards_for_unassignment(self): if not self.has_assignments(): # Reset its status appropriately. if self.status == ApplicationStatus.ASSIGNMENT_PENDING: - if ( - self.form.application_kind - == ApplicationKind.CONFIRM_THEN_ASSIGN - ): + if self.form.application_kind == ApplicationKind.CONFIRM_THEN_ASSIGN: self.status = ApplicationStatus.CONFIRMED else: self.status = ApplicationStatus.APPLIED diff --git a/stave/templates/stave/contexts.py b/stave/templates/stave/contexts.py index 0da25da..9f29f99 100644 --- a/stave/templates/stave/contexts.py +++ b/stave/templates/stave/contexts.py @@ -8,6 +8,7 @@ from stave import forms, models, avail + def to_dict(obj) -> dict: return {field.name: getattr(obj, field.name) for field in fields(obj)} diff --git a/stave/views.py b/stave/views.py index 290ebd8..5048570 100644 --- a/stave/views.py +++ b/stave/views.py @@ -1789,7 +1789,7 @@ def post( user=None, crew__kind=models.CrewKind.OVERRIDE_CREW, crew__role_group_override_assignments__game=game, - crew__role_group_id=role_group_id + crew__role_group_id=role_group_id, ).delete() redirect_url = request.POST.get("redirect_url") @@ -2026,7 +2026,6 @@ def get( ) - class CrewBuilderDetailView(LoginRequiredMixin, views.View): """A view rendering the Crew Builder with a list of applications for a given position. On GET, renders the view. @@ -2080,7 +2079,7 @@ def get( game=game, event=am.application_form.event, role=role, - ConflictKind=ConflictKind + ConflictKind=ConflictKind, ) ), ) @@ -2150,18 +2149,13 @@ def post( # If we are removing one user from a static-crew assignment, add a blank assignment. rgca = models.RoleGroupCrewAssignment.objects.filter( - role_group=role.role_group, - game=crew.get_context() - ).first() + role_group=role.role_group, game=crew.get_context() + ).first() if rgca and rgca.crew and not assignment and not applications: # rgca.crew is a static crew. # Override with a blank in the override crew. - models.CrewAssignment.objects.create( - role=role, - crew=crew, - user=None - ) + models.CrewAssignment.objects.create(role=role, crew=crew, user=None) if applications: # If the newly-selected user is already assigned to @@ -2171,7 +2165,9 @@ def post( # TODO: display current assignment on crew builder detail # TODO: show users who are time-available but not role-available - old_availability_entries = am.get_swappable_assignments(applications[0].user, crew, crew.get_context(), role) + old_availability_entries = am.get_swappable_assignments( + applications[0].user, crew, crew.get_context(), role + ) for avail_entry in old_availability_entries: # This UserAvailabilityEntry might come from a direct assignment # or from a static crew assignment; there are different ways @@ -2189,25 +2185,21 @@ def post( or avail_entry.crew.get_context() == crew.get_context() ) cas = models.CrewAssignment.objects.filter( - crew = avail_entry.crew, - user=applications[0].user + crew=avail_entry.crew, user=applications[0].user ) if keep_nonexclusive: cas = cas.exclude(role__nonexclusive=True) for ca in cas: models.CrewAssignment.objects.create( - role=ca.role, - crew=crew, - user=None + role=ca.role, crew=crew, user=None ) case models.CrewKind.OVERRIDE_CREW: # Just query for and delete the relevant CrewAssignments # If we're reassigning within the same crew, # just remove exclusive roles; otherwise, all roles. cas = models.CrewAssignment.objects.filter( - user=applications[0].user, - crew=avail_entry.crew + user=applications[0].user, crew=avail_entry.crew ) if crew == avail_entry.crew: cas = cas.exclude(role__nonexclusive=True) diff --git a/tests/test_avail.py b/tests/test_avail.py index c997e8d..b830e4f 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -6,109 +6,180 @@ from pytest import fixture from stave import models + @fixture def existing_entry(db): start_time = datetime.now(tz=timezone.utc) end_time = start_time + timedelta(hours=2) return UserAvailabilityEntry( - crew = CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), + crew=CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), start_time=start_time, end_time=end_time, - exclusive=True + exclusive=True, ) + + @fixture def existing_static_crew_entry(db): start_time = datetime.now(tz=timezone.utc) end_time = start_time + timedelta(hours=2) return UserAvailabilityEntry( - crew = CrewFactory(kind=models.CrewKind.GAME_CREW), + crew=CrewFactory(kind=models.CrewKind.GAME_CREW), start_time=start_time, end_time=end_time, - exclusive=True + exclusive=True, ) + def test_user_availability_entry__simple_overlap(existing_entry): - assert existing_entry.overlaps(UserAvailabilityEntry( - crew=existing_entry.crew, - start_time=existing_entry.start_time, - end_time=existing_entry.end_time, - exclusive=True - ), set()) == ConflictKind.SWAPPABLE_CONFLICT + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=True, + ), + set(), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + def test_user_availability_entry__simple_overlap_nonexclusive(existing_entry): - assert existing_entry.overlaps(UserAvailabilityEntry( - crew=existing_entry.crew, - start_time=existing_entry.start_time, - end_time=existing_entry.end_time, - exclusive=False - ), set()) == ConflictKind.NONE + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=False, + ), + set(), + ) + == ConflictKind.NONE + ) + def test_user_availability_entry__simple_overlap_other_role_group(existing_entry): - assert existing_entry.overlaps(UserAvailabilityEntry( - crew=CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), - start_time=existing_entry.start_time, - end_time=existing_entry.end_time, - exclusive=True - ), set()) == ConflictKind.NON_SWAPPABLE_CONFLICT + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.OVERRIDE_CREW), + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=True, + ), + set(), + ) + == ConflictKind.NON_SWAPPABLE_CONFLICT + ) + def test_user_availability_entry__swappable_overlap_other_role_group(existing_entry): other_crew = CrewFactory(kind=models.CrewKind.OVERRIDE_CREW) - assert existing_entry.overlaps(UserAvailabilityEntry( - crew=other_crew, - start_time=existing_entry.start_time, - end_time=existing_entry.end_time, - exclusive=False - ),set([other_crew.role_group])) == ConflictKind.SWAPPABLE_CONFLICT + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=other_crew, + start_time=existing_entry.start_time, + end_time=existing_entry.end_time, + exclusive=False, + ), + set([other_crew.role_group]), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + def test_user_availability_entry__partial_overlap(existing_entry): - assert existing_entry.overlaps(UserAvailabilityEntry( - crew=existing_entry.crew, - start_time=existing_entry.start_time + timedelta(hours=1), - end_time=existing_entry.end_time, - exclusive=True - ), set()) == ConflictKind.SWAPPABLE_CONFLICT + assert ( + existing_entry.overlaps( + UserAvailabilityEntry( + crew=existing_entry.crew, + start_time=existing_entry.start_time + timedelta(hours=1), + end_time=existing_entry.end_time, + exclusive=True, + ), + set(), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + + +def test_user_availability_entry__event_crews(): ... -def test_user_availability_entry__event_crews(): - ... def test_user_availability_entry__static_crews(existing_static_crew_entry): - assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( - crew=existing_static_crew_entry.crew, - start_time=None, - end_time=None, - exclusive=True - ), set()) == ConflictKind.SWAPPABLE_CONFLICT + assert ( + existing_static_crew_entry.overlaps( + UserAvailabilityEntry( + crew=existing_static_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=True, + ), + set(), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + def test_user_availability_entry__static_crews_nonexclusive(existing_static_crew_entry): - assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( - crew=existing_static_crew_entry.crew, - start_time=None, - end_time=None, - exclusive=False - ), set()) == ConflictKind.NONE - -def test_user_availability_entry__static_crews_other_role_group(existing_static_crew_entry): - assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( - crew=CrewFactory(kind=models.CrewKind.GAME_CREW), - start_time=None, - end_time=None, - exclusive=True - ), set()) == ConflictKind.NON_SWAPPABLE_CONFLICT + assert ( + existing_static_crew_entry.overlaps( + UserAvailabilityEntry( + crew=existing_static_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=False, + ), + set(), + ) + == ConflictKind.NONE + ) -def test_user_availability_entry__static_crews_swappable_role_group(existing_static_crew_entry): + +def test_user_availability_entry__static_crews_other_role_group( + existing_static_crew_entry, +): + assert ( + existing_static_crew_entry.overlaps( + UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.GAME_CREW), + start_time=None, + end_time=None, + exclusive=True, + ), + set(), + ) + == ConflictKind.NON_SWAPPABLE_CONFLICT + ) + + +def test_user_availability_entry__static_crews_swappable_role_group( + existing_static_crew_entry, +): other_crew = CrewFactory(kind=models.CrewKind.GAME_CREW) - assert existing_static_crew_entry.overlaps(UserAvailabilityEntry( - crew=CrewFactory(kind=models.CrewKind.GAME_CREW), - start_time=None, - end_time=None, - exclusive=True - ), set([other_crew.role_group])) == ConflictKind.SWAPPABLE_CONFLICT + assert ( + existing_static_crew_entry.overlaps( + UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.GAME_CREW), + start_time=None, + end_time=None, + exclusive=True, + ), + set([other_crew.role_group]), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + + +def test_user_availability_entry__override_of_static_crew(): ... + -def test_user_availability_entry__override_of_static_crew(): - ... +def test_user_availability_entry__non_meaningful(): ... -def test_user_availability_entry__non_meaningful(): - ... def test_availability_manager__applications(tournament): form = tournament.application_forms.get(slug="apply-nso-so") From d25bf1f8bc8350032da3ba536dd9e10bb30ba363 Mon Sep 17 00:00:00 2001 From: David Reed Date: Sun, 30 Nov 2025 20:51:49 -0700 Subject: [PATCH 15/26] Passing avail test --- tests/test_avail.py | 107 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 17 deletions(-) diff --git a/tests/test_avail.py b/tests/test_avail.py index b830e4f..91a007f 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -21,17 +21,24 @@ def existing_entry(db): @fixture def existing_static_crew_entry(db): - start_time = datetime.now(tz=timezone.utc) - end_time = start_time + timedelta(hours=2) return UserAvailabilityEntry( crew=CrewFactory(kind=models.CrewKind.GAME_CREW), - start_time=start_time, - end_time=end_time, + start_time=None, + end_time=None, + exclusive=True, + ) + +@fixture +def existing_event_crew_entry(db): + return UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.EVENT_CREW), + start_time=None, + end_time=None, exclusive=True, ) -def test_user_availability_entry__simple_overlap(existing_entry): +def test_user_availability_entry__full_overlap_same_crew_exclusive(existing_entry): assert ( existing_entry.overlaps( UserAvailabilityEntry( @@ -46,7 +53,7 @@ def test_user_availability_entry__simple_overlap(existing_entry): ) -def test_user_availability_entry__simple_overlap_nonexclusive(existing_entry): +def test_user_availability_entry__full_overlap_same_crew_nonexclusive(existing_entry): assert ( existing_entry.overlaps( UserAvailabilityEntry( @@ -61,7 +68,7 @@ def test_user_availability_entry__simple_overlap_nonexclusive(existing_entry): ) -def test_user_availability_entry__simple_overlap_other_role_group(existing_entry): +def test_user_availability_entry__full_overlap_other_crew_non_swappable(existing_entry): assert ( existing_entry.overlaps( UserAvailabilityEntry( @@ -76,7 +83,7 @@ def test_user_availability_entry__simple_overlap_other_role_group(existing_entry ) -def test_user_availability_entry__swappable_overlap_other_role_group(existing_entry): +def test_user_availability_entry__full_overlap_other_crew_swappable(existing_entry): other_crew = CrewFactory(kind=models.CrewKind.OVERRIDE_CREW) assert ( existing_entry.overlaps( @@ -107,10 +114,7 @@ def test_user_availability_entry__partial_overlap(existing_entry): ) -def test_user_availability_entry__event_crews(): ... - - -def test_user_availability_entry__static_crews(existing_static_crew_entry): +def test_user_availability_entry__overlap_same_static_crew_exclusive(existing_static_crew_entry): assert ( existing_static_crew_entry.overlaps( UserAvailabilityEntry( @@ -125,7 +129,7 @@ def test_user_availability_entry__static_crews(existing_static_crew_entry): ) -def test_user_availability_entry__static_crews_nonexclusive(existing_static_crew_entry): +def test_user_availability_entry__overlap_same_static_crew_nonexclusive(existing_static_crew_entry): assert ( existing_static_crew_entry.overlaps( UserAvailabilityEntry( @@ -140,7 +144,7 @@ def test_user_availability_entry__static_crews_nonexclusive(existing_static_crew ) -def test_user_availability_entry__static_crews_other_role_group( +def test_user_availability_entry__overlap_other_static_crew_non_swappable( existing_static_crew_entry, ): assert ( @@ -157,14 +161,14 @@ def test_user_availability_entry__static_crews_other_role_group( ) -def test_user_availability_entry__static_crews_swappable_role_group( +def test_user_availability_entry__overlap_other_static_crew_swappable( existing_static_crew_entry, ): other_crew = CrewFactory(kind=models.CrewKind.GAME_CREW) assert ( existing_static_crew_entry.overlaps( UserAvailabilityEntry( - crew=CrewFactory(kind=models.CrewKind.GAME_CREW), + crew=other_crew, start_time=None, end_time=None, exclusive=True, @@ -177,8 +181,77 @@ def test_user_availability_entry__static_crews_swappable_role_group( def test_user_availability_entry__override_of_static_crew(): ... +def test_user_availability_entry__overlap_same_event_crew_exclusive(existing_event_crew_entry): + assert ( + existing_event_crew_entry.overlaps( + UserAvailabilityEntry( + crew=existing_event_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=True, + ), + set(), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) -def test_user_availability_entry__non_meaningful(): ... +def test_user_availability_entry__overlap_same_event_crew_nonexclusive(existing_event_crew_entry): + assert ( + existing_event_crew_entry.overlaps( + UserAvailabilityEntry( + crew=existing_event_crew_entry.crew, + start_time=None, + end_time=None, + exclusive=False, + ), + set(), + ) + == ConflictKind.NONE + ) + +def test_user_availability_entry__overlap_other_event_crew_non_swappable(existing_event_crew_entry): + assert ( + existing_event_crew_entry.overlaps( + UserAvailabilityEntry( + crew=CrewFactory(kind=models.CrewKind.EVENT_CREW), + start_time=None, + end_time=None, + exclusive=True, + ), + set(), + ) + == ConflictKind.NON_SWAPPABLE_CONFLICT + ) + +def test_user_availability_entry__overlap_other_event_crew_swappable(existing_event_crew_entry): + other_crew = CrewFactory(kind=models.CrewKind.EVENT_CREW) + assert ( + existing_event_crew_entry.overlaps( + UserAvailabilityEntry( + crew=other_crew, + start_time=None, + end_time=None, + exclusive=True, + ), + set([other_crew.role_group]), + ) + == ConflictKind.SWAPPABLE_CONFLICT + ) + +def test_user_availability_entry__non_meaningful(existing_event_crew_entry): + other_crew = CrewFactory(kind=models.CrewKind.GAME_CREW) + assert ( + existing_event_crew_entry.overlaps( + UserAvailabilityEntry( + crew=other_crew, + start_time=None, + end_time=None, + exclusive=True, + ), + set(), + ) + == ConflictKind.NONE + ) def test_availability_manager__applications(tournament): From 589170ab60414b02717daa79d0311059ef12fbb3 Mon Sep 17 00:00:00 2001 From: David Reed Date: Wed, 3 Dec 2025 20:25:35 -0700 Subject: [PATCH 16/26] More tests --- stave/avail.py | 4 ---- tests/test_avail.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index d6b4b1d..5ce1473 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -306,10 +306,6 @@ def user_availability(self) -> dict[UUID, list[UserAvailabilityEntry]]: for rgca in game.role_group_crew_assignments.all(): effective_crew = rgca.effective_crew_by_role_id().values() for assignment in effective_crew: - # We squash all effective game assignments to appear - # as part of the override crew. This ensures we - # catch conflicts between the assigned static crew - # and the overrides. if assignment.user_id: user_assigned_times_map[assignment.user_id].append( UserAvailabilityEntry( diff --git a/tests/test_avail.py b/tests/test_avail.py index 91a007f..45ae751 100644 --- a/tests/test_avail.py +++ b/tests/test_avail.py @@ -277,3 +277,46 @@ def test_availability_manager__applications(tournament): # TODO: test exclusion by application status # TODO: test that prefetches cache + # + + def test_availability_manager__applications_by_status(db): + application_form = ApplicationFormFactory() + for status in models.ApplicationStatus: + for _ in range(3): + ApplicationFactory(application_form=application_form, status=status) + + am = AvailabilityManager.with_application_form(application_form) + by_status = am.applications_by_status() + + assert keys(by_status) == list(models.ApplicationStatus) + assert all(len(v) == 3 for v in by_status.values()) + + def test_availability_manager__get_applications_in_statuses(db): + application_form = ApplicationFormFactory() + for status in models.ApplicationStatus: + for i in range(3): + ApplicationFactory(user__preferred_name="CBA"[i], application_form=application_form, status=status) + + am = AvailabilityManager.with_application_form(application_form) + in_statuses = am.get_applications_in_statuses((models.ApplicationStatus.APPLIED, models.ApplicationStatus.ASSIGNED)) + + assert len(in_statuses) == 6 + assert in_statuses == sorted(in_statuses, key=lambda a: a.user.preferred_name) + + def test_availability_manager__static_crews(db): + application_form = ApplicationFormFactory() + crew = CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) + CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) + CrewFactory(kind=models.CrewKind.GAME_CREW) + am = AvailabilityManager.with_application_form(application_form) + + assert am.static_crews == [crew] + + def test_availability_manager__event_crews(db): + application_form = ApplicationFormFactory() + crew = CrewFactory(event=application_form.event, kind=models.CrewKind.EVENT_CREW) + CrewFactory(event=application_form.event, kind=models.CrewKind.GAME_CREW) + CrewFactory(kind=models.CrewKind.EVENT_CREW) + am = AvailabilityManager.with_application_form(application_form) + + assert am.event_crews == [crew] From 1b2e7bb81e4e6b9bafef94ddae563985fd2a886e Mon Sep 17 00:00:00 2001 From: David Reed Date: Tue, 9 Dec 2025 21:40:21 -0700 Subject: [PATCH 17/26] Big refactor with more tests --- stave/avail.py | 200 ++++++++++++++---- stave/templates/stave/contexts.py | 9 +- .../templates/stave/crew_builder_detail.html | 2 +- stave/templates/stave/form_applications.html | 8 +- .../templates/stave/partials/crew_editor.html | 2 +- stave/views.py | 157 ++++---------- tests/test_avail.py | 27 +++ 7 files changed, 245 insertions(+), 160 deletions(-) diff --git a/stave/avail.py b/stave/avail.py index 5ce1473..d96fe21 100644 --- a/stave/avail.py +++ b/stave/avail.py @@ -2,7 +2,7 @@ from collections import defaultdict from dataclasses import dataclass from datetime import datetime -from typing import Iterable +from typing import Iterable,Generator, Tuple from uuid import UUID import enum @@ -19,6 +19,8 @@ class ConflictKind(enum.Enum): ConflictKind.do_not_call_in_templates = True +class UserNotAvailableException(Exception): + pass @dataclass class ApplicationEntry: @@ -41,51 +43,54 @@ def overlaps( other: "UserAvailabilityEntry", swappable_role_groups: set[models.RoleGroup], ) -> ConflictKind: - if self.crew.role_group_id == other.crew.role_group_id: - if self.exclusive and other.exclusive: - return ConflictKind.SWAPPABLE_CONFLICT - else: - return ConflictKind.NONE - has_times = None not in ( self.start_time, self.end_time, other.start_time, other.end_time, ) + if ( + self.start_time == other.start_time + and self.end_time == other.end_time + and self.crew.role_group == other.crew.role_group + ): + # This is a self-swap: either the same crew entirely, + # or an override crew and a static crew assigned to the + # same game. That means we need to account for role + # exclusiveness: the two roles could be complementary. + if self.exclusive and other.exclusive: + return ConflictKind.SWAPPABLE_CONFLICT + else: + return ConflictKind.NONE - match (self.crew.kind, other.crew.kind, has_times): - case (models.CrewKind.OVERRIDE_CREW, models.CrewKind.OVERRIDE_CREW, _) | ( - models.CrewKind.GAME_CREW, - models.CrewKind.OVERRIDE_CREW, - True, - ): + elif has_times: # This is a comparison between two override crews, which always # have defined start and end times, or between two assigned static crews # or an assigned static crew and an override crew, which will # also have times. - time_overlap = (self.start_time < other.end_time) and ( - self.end_time > other.start_time + time_overlap = ( + self.start_time < other.end_time + and self.end_time > other.start_time ) + if time_overlap: - if other.crew.role_group in swappable_role_groups: + if ( + other.crew.role_group in swappable_role_groups + or other.crew.role_group == self.crew.role_group + ): return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NON_SWAPPABLE_CONFLICT - case (models.CrewKind.EVENT_CREW, models.CrewKind.EVENT_CREW, False) | ( - models.CrewKind.GAME_CREW, - models.CrewKind.GAME_CREW, - False, - ): + elif self.crew.kind == other.crew.kind: # This comparison is between static crews _without_ assignments # or between event crews. No times involved. if other.crew.role_group in swappable_role_groups: return ConflictKind.SWAPPABLE_CONFLICT else: return ConflictKind.NON_SWAPPABLE_CONFLICT - case _: - return ConflictKind.NONE + + return ConflictKind.NONE class ScheduleManager: @@ -297,24 +302,28 @@ def event_crews(self) -> list[models.Crew]: if crew.kind == models.CrewKind.EVENT_CREW ] + @property + def game_crew_assignments(self) -> Generator[Tuple[models.Game, models.CrewAssignment]]: + for game in self.application_form.event.games.all(): + for rgca in game.role_group_crew_assignments.all(): + for ca in rgca.effective_crew_by_role_id().values(): + yield (game, ca) + @property @functools.cache def user_availability(self) -> dict[UUID, list[UserAvailabilityEntry]]: user_assigned_times_map = defaultdict(list) - for game in self.application_form.event.games.all(): - for rgca in game.role_group_crew_assignments.all(): - effective_crew = rgca.effective_crew_by_role_id().values() - for assignment in effective_crew: - if assignment.user_id: - user_assigned_times_map[assignment.user_id].append( - UserAvailabilityEntry( - crew=assignment.crew, - start_time=game.start_time, - end_time=game.end_time, - exclusive=not assignment.role.nonexclusive, - ) - ) + for (game, assignment) in self.game_crew_assignments: + if assignment.user_id: + user_assigned_times_map[assignment.user_id].append( + UserAvailabilityEntry( + crew=assignment.crew, + start_time=game.start_time, + end_time=game.end_time, + exclusive=not assignment.role.nonexclusive, + ) + ) return user_assigned_times_map @@ -470,8 +479,6 @@ def get_swappable_assignments( if t.overlaps(entry, self.role_groups) is ConflictKind.SWAPPABLE_CONFLICT ] - # Filter methods MUST NOT hit the database - use only cached data - def _get_avail_data_for_crew_kind( self, kind: models.CrewKind ) -> dict[UUID, list[UserAvailabilityEntry]]: @@ -514,3 +521,120 @@ def _filter_for_basic_availability( ] return applications + + def get_application_by_id(self, id: UUID) -> models.Application | None: + for application in self.applications: + if application.id== id and application.status != models.ApplicationStatus.WITHDRAWN: + return application + + def get_application_for_user(self, user: models.User) -> models.Application | None: + for application in self.applications: + if application.user == user and application.status != models.ApplicationStatus.WITHDRAWN: + return application + + def get_application_for_assignment(self, assignment: models.CrewAssignment) -> models.Application | None: + # There should be exactly one or zero non-withdrawn applications for this user. + if assignment.user: + return self.get_application_for_user(assignment.user) + + def get_assignment( + self, + role: models.Role, + crew: models.Crew, + ) -> models.CrewAssignment | None: + for ca in crew.assignments.all(): + if ca.role == role: + return ca + + def set_assignment( + self, + role: models.Role, + crew: models.Crew, + user: models.User | None, + ): + # Regardless of the "to" side of this assignment, + # we need to remove any existing assignment. + existing_assignment = self.get_assignment(role, crew) + if existing_assignment: + application = self.get_application_for_assignment(existing_assignment) + existing_assignment.delete() + if application: + application.move_status_backwards_for_unassignment() + + # If necessary, swap the new user out of their existing assignments. + if user: + application = self.get_application_for_user(user) + if application: + application.move_status_forwards_for_assignment() + else: + raise UserNotAvailableException("There is no application for user") + + old_availability_entries = self.get_swappable_assignments( + user, crew, crew.get_context(), role + ) + for avail_entry in old_availability_entries: + # This UserAvailabilityEntry might come from a direct assignment + # or from a static crew assignment; there are different ways + # to replace those. + # FIXME: could we get back a static crew entry when we are blanking + # an override? + match avail_entry.crew.kind: + case models.CrewKind.GAME_CREW | models.CrewKind.EVENT_CREW: + keep_nonexclusive = ( + # Swap within a crew + avail_entry.crew == crew + # Swap from a static crew to an override crew in the same context + or avail_entry.crew.get_context() == crew.get_context() + ) + case models.CrewKind.OVERRIDE_CREW: + # If we're reassigning within the same crew, + # just remove exclusive roles; otherwise, all roles. + keep_nonexclusive = avail_entry.crew == crew + + # This is a set, not a list, because when we're overriding a static crew + # assignment, we'll get back a CrewAssignment for every game that the + # static crew is assigned to from game_crew_assignments + cas = { + ca + for (_, ca) in self.game_crew_assignments + if ( + ca.crew == avail_entry.crew + and ca.user == user + ) + } + if keep_nonexclusive: + cas = {ca for ca in cas if not ca.role.nonexclusive} + + # Add an overriding CrewAssignment to blank for each relevant + # assignment. This ensures that, if we are removing an override + # crew assignment, any underlying static crew member is not + # silently re-staffed into this role. + # If this is an override crew, also delete the original CrewAssignment. + # + # Note that `crew` is not necessarily `avail_entry.crew`. + # If the latter is a static crew, the former is an override crew. + breakpoint() + for ca in cas: + if avail_entry.crew.kind == models.CrewKind.OVERRIDE_CREW: + ca.delete() + models.CrewAssignment.objects.create( + role=ca.role, crew=crew, user=None + ) + # FIXME: we could then detect if we're assigning user X + # when a static crew would assign X, and do nothing. + + # Finally, add the new entry. + # Note that this might be a null override on top of a static crew, + # including the case where we're blanking a static crew assignment. + new_assignment = models.CrewAssignment.objects.create( + role=role, crew=crew, user=user + ) + + def set_crew_assignment( + self, + role_group: models.RoleGroup, + crew: models.Crew, + game: models.Game, + ): + # FIXME: must remove any overrides that exist. + ... diff --git a/stave/templates/stave/contexts.py b/stave/templates/stave/contexts.py index 9f29f99..72670f2 100644 --- a/stave/templates/stave/contexts.py +++ b/stave/templates/stave/contexts.py @@ -87,11 +87,10 @@ class ViewApplicationContext: @dataclass class FormApplicationsInputs: form: models.ApplicationForm - applications_action: QuerySet[models.Application] - applications_inprogress: QuerySet[models.Application] - applications_staffed: QuerySet[models.Application] - applications_closed: QuerySet[models.Application] - game_counts: dict[UUID, int] + applications_action: list[avail.ApplicationEntry] + applications_inprogress: list[avail.ApplicationEntry] + applications_staffed: list[avail.ApplicationEntry] + applications_closed: list[avail.ApplicationEntry] ApplicationStatus: type diff --git a/stave/templates/stave/crew_builder_detail.html b/stave/templates/stave/crew_builder_detail.html index 8ad56be..4812fc3 100644 --- a/stave/templates/stave/crew_builder_detail.html +++ b/stave/templates/stave/crew_builder_detail.html @@ -21,7 +21,7 @@

{% csrf_token %} - + {% if entry.availability_status == ConflictKind.NONE %} {% else %} diff --git a/stave/templates/stave/form_applications.html b/stave/templates/stave/form_applications.html index 75d0f5c..3fce39c 100644 --- a/stave/templates/stave/form_applications.html +++ b/stave/templates/stave/form_applications.html @@ -35,14 +35,12 @@ {% partialdef application-table %} - {% for application in applications %} + {% for entry in entries %}

- {% include "stave/application_actions.html" with user=request.user can_manage_event=can_manage_event application=application ApplicationStatus=ApplicationStatus include_view=True minimal=True only %} + {% include "stave/application_actions.html" with user=request.user can_manage_event=can_manage_event application=entry.application ApplicationStatus=ApplicationStatus include_view=True minimal=True only %}