diff --git a/Dockerfile b/Dockerfile index e91088b1..6cc40c9c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ FROM python:3.12-slim as common-base #ENV DJANGO_SETTINGS_MODULE foo.settings ENV UID=2008 +ENV PATH="/opt/bitpoll/.venv/bin:$PATH" RUN usermod -u $UID -g nogroup -d /opt/bitpoll www-data @@ -19,11 +20,12 @@ RUN pip install -U pip setuptools FROM base-builder as dependencies -RUN apt-get update && apt-get -y --no-install-recommends install g++ wget python3-pip make gettext gcc python3-dev libldap2-dev gpg gpg-agent curl libsasl2-dev npm +RUN apt-get update && apt-get -y --no-install-recommends install g++ wget python3-pip make gettext gcc python3-dev libldap2-dev gpg gpg-agent curl libsasl2-dev npm && \ + pip install --no-cache-dir uv -COPY requirements-production.txt . +COPY pyproject.toml uv.lock README.md . -RUN URLLIB3_NO_OVERRIDE=1 pip install --no-warn-script-location --prefix=/install -U --no-binary urllib3-future -r requirements-production.txt +RUN uv sync --locked --no-dev --extra production --no-install-project FROM dependencies as collect-static @@ -35,17 +37,16 @@ COPY bitpoll bitpoll COPY locale locale COPY docker_files/config/settings.py bitpoll/settings_local.py -# Set Pythonpath tro the packages installed with pip bevore so they are aviable in this step -RUN export PYTHONPATH=/install/lib/python$(python3 --version | cut -d ' ' -f 2 | cut -d '.' -f 1,2)/site-packages && \ - python3 /opt/bitpoll/manage.py collectstatic --noinput && \ - python3 manage.py compilemessages &&\ +# Dependencies are installed in /opt/bitpoll/.venv by uv in the previous stage. +RUN uv run --locked /opt/bitpoll/manage.py collectstatic --noinput && \ + uv run --locked manage.py compilemessages &&\ rm bitpoll/settings_local.py FROM common-base #RUN apt-get -y --no-install-recommends install python3-psycopg2 python3-ldap3 gettext -COPY --from=dependencies /install /usr/local +COPY --from=dependencies /opt/bitpoll/.venv /opt/bitpoll/.venv COPY --from=collect-static /opt/bitpoll . COPY docker_files/run /usr/local/bin diff --git a/README.md b/README.md index 0a61ed90..9155b30e 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,10 @@ Get the code: git clone https://github.com/fsinfuhh/Bitpoll ``` -Generate a Python virtualenv and install dependencies: +Install Python and development dependencies with uv: ```bash -virtualenv -p $(which python3) .pyenv -source .pyenv/bin/activate -pip install -r requirements.txt +uv sync --group dev ``` Copy `bitpoll/settings_local.sample.py` to `bitpoll/settings_local.py` and customize the local settings. @@ -94,9 +92,15 @@ Run Testserver: ./manage.py runserver ``` +Run the test suite: + +```bash +uv run pytest +``` + ### Production -In production Senty is used for error reporting. +In production Sentry is used for error reporting. django-auth-ldap is used vor login via ldap uwsgi to serve the app @@ -109,7 +113,7 @@ sudo apt install g++ make python3-psycopg2 python3-ldap3 gettext gcc python3-dev Install Python Dependencies ```bash -pip install -r requirements-production.txt +uv sync --extra production ``` Configure examples are in `settings_local.py` @@ -126,18 +130,21 @@ For Production systems it is nessesarry to run ## Management of Dependencies -We use pip-tools to manage the dependencies. -After modification or the requirements*.in files or for updates of packages run +We use uv to manage and lock the dependencies. Runtime dependencies are in +`pyproject.toml`; production-only dependencies are in the `production` extra +and test tooling is in the `dev` dependency group. + +After changing dependencies or for package updates, run: ```bash -pip-compile --upgrade --output-file requirements.txt requirements.in -pip-compile --upgrade --output-file requirements-production.txt requirements-production.in requirements.in +uv lock --upgrade ``` -to sync your enviroment with the requirements.txt just run +Synchronize the local environment with the lockfile using: ```bash -pip-sync +uv sync --group dev ``` -this will install/deinstall dependencies so that the virtualenv is matching the requirements file +For a production environment use `uv sync --extra production`. The committed +`uv.lock` makes both environments reproducible. diff --git a/bitpoll/base/templatetags/settings_value.py b/bitpoll/base/templatetags/settings_value.py index 8e890ab0..1b04a807 100644 --- a/bitpoll/base/templatetags/settings_value.py +++ b/bitpoll/base/templatetags/settings_value.py @@ -1,9 +1,5 @@ -from django import template from django.conf import settings - -from django.template import TemplateSyntaxError, Variable, Node, Variable, Library - -register = template.Library() +from django.template import TemplateSyntaxError, Variable, Node, Library register = Library() diff --git a/bitpoll/base/tests/__init__.py b/bitpoll/base/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/base/tests/test_post.py b/bitpoll/base/tests/test_post.py new file mode 100644 index 00000000..da2a2694 --- /dev/null +++ b/bitpoll/base/tests/test_post.py @@ -0,0 +1,56 @@ +import pytest +from model_bakery import baker +from django.forms.models import model_to_dict +from django.urls import reverse + +from bitpoll.base.models import BitpollUser +from bitpoll.poll.models import ChoiceValue, Poll, PollWatch, Vote + + +pytestmark = pytest.mark.django_db + + +def test_poll_creation_post_creates_poll_and_values(client): + user = baker.make("base.BitpollUser", _fill_optional=True, username="poll-creator") + client.force_login(user) + data = { + "title": "Created poll", + "type": "universal", + "public_listening": "", + "due_date": "", + "url": "created-poll", + "description": "Created through POST", + "anonymous_allowed": "on", + "require_login": "", + "require_invitation": "", + "allow_unauthenticated_vote_changes": "on", + "one_vote_per_user": "on", + "vote_all": "", + } + + response = client.post(reverse("index"), data) + + assert response.status_code == 302 + poll = Poll.objects.get(url="created-poll") + assert poll.user == user + assert ChoiceValue.objects.filter(poll=poll).count() == 4 + + +def test_user_settings_post_updates_user_and_watch(client): + user = baker.make("base.BitpollUser", _fill_optional=True, username="settings-user") + poll = baker.make(Poll, _fill_optional=True, user=user, url="settings-poll", due_date=None) + baker.make(Vote, _fill_optional=True, poll=poll, user=user) + client.force_login(user) + data = model_to_dict(user, fields=["auto_watch", "email_invitation", "timezone", "language"]) + data.update({"auto_watch": True, "email_invitation": False, "timezone": "UTC", "language": "english"}) + + response = client.post(reverse("settings"), data) + + assert response.status_code == 200 + user.refresh_from_db() + assert user.auto_watch is True + assert user.timezone == "UTC" + assert PollWatch.objects.filter(poll=poll, user=user).exists() + + + diff --git a/bitpoll/base/tests/test_simple_get.py b/bitpoll/base/tests/test_simple_get.py new file mode 100644 index 00000000..b5a49a6c --- /dev/null +++ b/bitpoll/base/tests/test_simple_get.py @@ -0,0 +1,29 @@ +import pytest + +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +pytestmark = pytest.mark.django_db + +URLS = get_module_urls("base") + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, url_name, arg_count): + response = client.get(get_dynamic_url([], url_name)) + match url_name: + case "settings" | "base_autocomplete": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + +@pytest.mark.django_db +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, django_user_model, url_name, arg_count): + user = django_user_model.objects.create_user(username="base-user", password="password") + client.force_login(user) + response = client.get(get_dynamic_url([], url_name)) + assert response.status_code == 200 + + diff --git a/bitpoll/caldav/tests/__init__.py b/bitpoll/caldav/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/caldav/tests/test_post.py b/bitpoll/caldav/tests/test_post.py new file mode 100644 index 00000000..b9e9dfc3 --- /dev/null +++ b/bitpoll/caldav/tests/test_post.py @@ -0,0 +1,65 @@ +import pytest +from model_bakery import baker +from django.urls import reverse + +from bitpoll.caldav.models import DavCalendar + + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def calendar_data(settings): + settings.FIELD_ENCRYPTION_KEY = "this+is+an+example+key+please+generate+one+=" + user = baker.make("base.BitpollUser", _fill_optional=True, username="caldav-user") + calendar = baker.make( + DavCalendar, + _fill_optional=True, + user=user, + url="https://calendar.example/", + name="Test calendar", + ) + return user, calendar + + +def test_delete_calendar_post_deletes_calendar(client, calendar_data): + user, calendar = calendar_data + client.force_login(user) + + response = client.post( + reverse("change_calendar"), + {"delete": str(calendar.pk)}, + ) + + assert response.status_code == 302 + assert not DavCalendar.objects.filter(pk=calendar.pk).exists() + + +def test_create_calendar_post_saves_calendar(client, calendar_data, monkeypatch): + user, _ = calendar_data + client.force_login(user) + + class FakeCalendar: + def __init__(self, **kwargs): + pass + + def date_search(self, *args): + return [] + + monkeypatch.setattr("bitpoll.caldav.views.Calendar", FakeCalendar) + monkeypatch.setattr("bitpoll.caldav.views.DAVClient", lambda url: object()) + + response = client.post( + reverse("change_calendar"), + { + "url_0": "https://new-calendar.example/", + "url_1": "", + "url_2": "", + "name": "New calendar", + }, + ) + + assert response.status_code == 302 + assert DavCalendar.objects.filter(user=user, name="New calendar").exists() + + diff --git a/bitpoll/caldav/tests/test_simple_get.py b/bitpoll/caldav/tests/test_simple_get.py new file mode 100644 index 00000000..b3b65840 --- /dev/null +++ b/bitpoll/caldav/tests/test_simple_get.py @@ -0,0 +1,45 @@ +import pytest + +from bitpoll.caldav.models import DavCalendar +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +URLS = get_module_urls("caldav") + + +@pytest.fixture +def calendar_data(db, django_user_model, settings): + settings.FIELD_ENCRYPTION_KEY = "this+is+an+example+key+please+generate+one+=" + user = django_user_model.objects.create_user(username="caldav-user", password="password") + calendar = DavCalendar.objects.create(user=user, url="https://calendar.example/", name="Test calendar") + return user, calendar + + +def url_args(url_name, calendar): + return [calendar.pk] if "edit" in url_name else [] + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, calendar_data, url_name, arg_count): + _, calendar = calendar_data + response = client.get(get_dynamic_url(url_args(url_name, calendar), url_name)) + match url_name: + case "change_calendar" | "edit_save_calendar": + assert response.status_code == 405 + case "edit_calendar": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, calendar_data, url_name, arg_count): + user, calendar = calendar_data + client.force_login(user) + response = client.get(get_dynamic_url(url_args(url_name, calendar), url_name)) + match url_name: + case "change_calendar" | "edit_save_calendar": + assert response.status_code == 405 + case _: + assert response.status_code == 200 + diff --git a/bitpoll/caldav/utils.py b/bitpoll/caldav/utils.py index ab7db758..7ca36ecf 100644 --- a/bitpoll/caldav/utils.py +++ b/bitpoll/caldav/utils.py @@ -59,7 +59,7 @@ def get_caldav(choices: List[Choice], current_poll: Poll, user: BitpollUser, req pass cache.set(cache_key, events_calendar) except AuthorizationError as e: - messages.warning(request, ugettext_lazy('Could not access your calendar "%s" due to an authorization error' % calendar_obj.name)) + messages.warning(request, gettext_lazy('Could not access your calendar "%s" due to an authorization error' % calendar_obj.name)) events += events_calendar for choice in choices: ev_tmp = [] diff --git a/bitpoll/groups/forms.py b/bitpoll/groups/forms.py index 26e2ed43..60e69ad4 100644 --- a/bitpoll/groups/forms.py +++ b/bitpoll/groups/forms.py @@ -48,4 +48,5 @@ def get_invitations(self): return invitations def save(self): - self.get_invitation().save() + for invitation in self.get_invitations(): + invitation.save() diff --git a/bitpoll/groups/models.py b/bitpoll/groups/models.py index 81f5f761..fad5ee86 100644 --- a/bitpoll/groups/models.py +++ b/bitpoll/groups/models.py @@ -69,7 +69,7 @@ class GroupInvitation(models.Model): invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='given_invitations', on_delete=models.CASCADE) - def __unicode__(self): + def __str__(self): return u'Invitation to {0} for {1}'.format(self.group, self.invitee) def accept(self): diff --git a/bitpoll/groups/tests/__init__.py b/bitpoll/groups/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/groups/tests/test_post.py b/bitpoll/groups/tests/test_post.py new file mode 100644 index 00000000..cbd2d7e4 --- /dev/null +++ b/bitpoll/groups/tests/test_post.py @@ -0,0 +1,117 @@ +import pytest +from model_bakery import baker +from django.contrib.auth.models import Group +from django.urls import reverse + +from bitpoll.groups.models import GroupInvitation + + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def group_data(): + admin = baker.make("base.BitpollUser", _fill_optional=True, username="group-admin") + member = baker.make("base.BitpollUser", _fill_optional=True, username="group-member") + group = baker.make(Group, _fill_optional=True, name="test-group") + group.user_set.add(admin, member) + group.properties.admins.add(admin) + invitation = baker.make( + GroupInvitation, + _fill_optional=True, + group=group, + invitee=member, + invited_by=admin, + ) + return admin, member, group, invitation + + +def url(name, *args): + return reverse(name, args=args) + + +def test_create_post_creates_group(client, admin_user=None): + user = baker.make("base.BitpollUser", _fill_optional=True, username="creator") + client.force_login(user) + + response = client.post(url("groups_create"), {"group_name": "new-group"}) + + assert response.status_code == 302 + group = Group.objects.get(name="new-group") + assert group.user_set.filter(pk=user.pk).exists() + assert group.properties.admins.filter(pk=user.pk).exists() + + +def test_leave_post_removes_member(client, group_data): + admin, member, group, _ = group_data + client.force_login(member) + + response = client.post(url("groups_leave", group.name)) + + assert response.status_code == 302 + assert not group.user_set.filter(pk=member.pk).exists() + + +def test_group_action_grants_admin(client, group_data): + admin, member, group, _ = group_data + client.force_login(admin) + + response = client.post( + url("groups_action", group.name, member.pk), + {"grant_admin": "Grant"}, + ) + + assert response.status_code == 302 + assert group.properties.admins.filter(pk=member.pk).exists() + + +def test_group_action_kicks_member(client, group_data): + admin, member, group, _ = group_data + client.force_login(admin) + + response = client.post( + url("groups_action", group.name, member.pk), + {"kick": "Kick"}, + ) + + assert response.status_code == 302 + assert not group.user_set.filter(pk=member.pk).exists() + + +def test_invitation_accept_post_adds_member(client, group_data): + admin, member, group, invitation = group_data + group.user_set.remove(member) + client.force_login(member) + + response = client.post( + url("groups_invitation_action", invitation.pk), + {"accept": "Accept"}, + ) + + assert response.status_code == 302 + assert group.user_set.filter(pk=member.pk).exists() + assert not GroupInvitation.objects.filter(pk=invitation.pk).exists() + + +def test_invitation_refuse_post_deletes_invitation(client, group_data): + admin, member, group, invitation = group_data + client.force_login(member) + + response = client.post( + url("groups_invitation_action", invitation.pk), + {"refuse": "Refuse"}, + ) + + assert response.status_code == 302 + assert not GroupInvitation.objects.filter(pk=invitation.pk).exists() + + +def test_withdraw_invite_post_deletes_invitation(client, group_data): + admin, member, group, invitation = group_data + client.force_login(admin) + + response = client.post(url("groups_withdraw_invite", invitation.pk)) + + assert response.status_code == 302 + assert not GroupInvitation.objects.filter(pk=invitation.pk).exists() + diff --git a/bitpoll/groups/tests/test_simple_get.py b/bitpoll/groups/tests/test_simple_get.py new file mode 100644 index 00000000..f84d7099 --- /dev/null +++ b/bitpoll/groups/tests/test_simple_get.py @@ -0,0 +1,56 @@ +import pytest +from django.contrib.auth.models import Group + +from bitpoll.groups.models import GroupInvitation +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +URLS = get_module_urls("groups") + + +@pytest.fixture +def group_data(db, django_user_model): + user = django_user_model.objects.create_user(username="group-user", password="password") + group = Group.objects.create(name="test-group") + group.user_set.add(user) + group.properties.admins.add(user) + invitation = GroupInvitation.objects.create(group=group, invitee=user, invited_by=user) + return user, group, invitation + + +def url_args(url_name, group, invitation): + if "groups_action" in url_name: + return [group.name, group.user_set.first().pk] + if "invitation" in url_name or "withdraw" in url_name: + return [invitation.pk] + if "groups_" in url_name and url_name != "groups_index" and url_name != "groups_create": + return [group.name] + return [] + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, group_data, url_name, arg_count): + _, group, invitation = group_data + response = client.get(get_dynamic_url(url_args(url_name, group, invitation), url_name)) + match url_name: + case "groups_index" | "groups_create" | "groups_show" | "groups_leave" | "groups_invite" | "groups_action" | "groups_invitation_action" | "groups_withdraw_invite": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, group_data, url_name, arg_count): + user, group, invitation = group_data + client.force_login(user) + response = client.get(get_dynamic_url(url_args(url_name, group, invitation), url_name)) + match url_name: + case "groups_index": + assert response.status_code == 302 + case "groups_leave" | "groups_action" | "groups_invitation_action" | "groups_withdraw_invite": + assert response.status_code == 405 + case _: + assert response.status_code == 200 + + + diff --git a/bitpoll/groups/urls.py b/bitpoll/groups/urls.py index b15054dd..18f5b8ce 100644 --- a/bitpoll/groups/urls.py +++ b/bitpoll/groups/urls.py @@ -10,8 +10,8 @@ re_path(r'^g/([a-zA-Z0-9_\-\.]+)/invite$', invite, name='groups_invite'), re_path(r'^g/([a-zA-Z0-9_\-\.]+)/action/(\d+)$', group_action, name='groups_action'), - re_path(r'^invitations/(\d+)/action', invitation_action, + re_path(r'^invitations/(\d+)/action$', invitation_action, name='groups_invitation_action'), - re_path(r'^invitations/(\d+)/withdraw', withdraw_invite, + re_path(r'^invitations/(\d+)/withdraw$', withdraw_invite, name='groups_withdraw_invite'), ] diff --git a/bitpoll/groups/views.py b/bitpoll/groups/views.py index d4737ac3..c67bbaa1 100644 --- a/bitpoll/groups/views.py +++ b/bitpoll/groups/views.py @@ -130,7 +130,7 @@ def group_action(request, group_name, member_pk): group_proxy.remove_member(member, check_sole_admin=True) messages.success(request, _('User was removed from group')) except GroupError as e: - messages.errror(request, e.message) + messages.error(request, e.message) elif 'grant_admin' in request.POST: group_proxy.grant_admin(member) messages.success(request, _('User was granted group admin.')) @@ -139,7 +139,7 @@ def group_action(request, group_name, member_pk): group_proxy.revoke_admin(member) messages.success(request, _('Revoked group admin rights from user.')) except GroupError as e: - messages.errror(request, e.message) + messages.error(request, e.message) return redirect('groups_show', group.name) diff --git a/bitpoll/invitations/admin.py b/bitpoll/invitations/admin.py index 183fa1fe..cb2ec11e 100644 --- a/bitpoll/invitations/admin.py +++ b/bitpoll/invitations/admin.py @@ -2,5 +2,7 @@ from .models import Invitation -# Register your models here. -admin.site.register(Invitation) \ No newline at end of file + +@admin.register(Invitation) +class InvitationAdmin(admin.ModelAdmin): + raw_id_fields = ('creator', 'user', 'poll', 'vote') \ No newline at end of file diff --git a/bitpoll/invitations/migrations/0005_alter_invitation_date_created.py b/bitpoll/invitations/migrations/0005_alter_invitation_date_created.py new file mode 100644 index 00000000..a9336aac --- /dev/null +++ b/bitpoll/invitations/migrations/0005_alter_invitation_date_created.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2026-03-01 18:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0004_auto_20170315_2113'), + ] + + operations = [ + migrations.AlterField( + model_name='invitation', + name='date_created', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/bitpoll/invitations/models.py b/bitpoll/invitations/models.py index 021a1178..c39fcfcd 100644 --- a/bitpoll/invitations/models.py +++ b/bitpoll/invitations/models.py @@ -19,7 +19,7 @@ class Invitation(models.Model): class Meta: unique_together = ('user', 'poll') - date_created = models.DateTimeField() + date_created = models.DateTimeField(auto_now_add=True) creator = models.ForeignKey(BitpollUser, on_delete=models.CASCADE, related_name='creator') user = models.ForeignKey(BitpollUser, on_delete=models.CASCADE) poll = models.ForeignKey(Poll, on_delete=models.CASCADE) @@ -43,12 +43,12 @@ def send(self, request): except SMTPRecipientsRefused: translation.activate(old_lang) messages.error( - request, _("The mail server had an error sending the notification to {}".format(self.user.username)) + request, _("The mail server had an error sending the notification to {}").format(self.user.username) ) translation.activate(old_lang) else: messages.error( - request, _("You have send an Email for {} in the last 12 Hours".format(self.user.username)) + request, _("You have sent an email for {} in the last 12 hours").format(self.user.username) ) def can_edit(self, user: BitpollUser, request: HttpRequest=None): diff --git a/bitpoll/invitations/tests/__init__.py b/bitpoll/invitations/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/invitations/tests/test_post.py b/bitpoll/invitations/tests/test_post.py new file mode 100644 index 00000000..e2ffadd9 --- /dev/null +++ b/bitpoll/invitations/tests/test_post.py @@ -0,0 +1,64 @@ +import pytest +from model_bakery import baker +from django.urls import reverse + +from bitpoll.invitations.models import Invitation +from bitpoll.poll.models import Poll + + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def invitation_data(): + owner = baker.make("base.BitpollUser", _fill_optional=True, username="invitation-owner") + invitee = baker.make("base.BitpollUser", _fill_optional=True, username="invitation-user") + poll = baker.make( + Poll, + _fill_optional=True, + title="Invitation poll", + url="invitation-poll", + type="universal", + user=owner, + require_login=False, + require_invitation=False, + due_date=None, + ) + invitation = baker.make( + Invitation, + _fill_optional=True, + creator=owner, + user=invitee, + poll=poll, + vote=None, + ) + return owner, invitee, poll, invitation + + +def test_invitation_delete_post_deletes_invitation(client, invitation_data): + owner, invitee, poll, invitation = invitation_data + client.force_login(owner) + + response = client.post( + reverse("invitations", args=[poll.url]), + {"delete": str(invitation.pk)}, + ) + + assert response.status_code == 302 + assert not Invitation.objects.filter(pk=invitation.pk).exists() + + +def test_invitation_send_post_creates_invitation(client, invitation_data): + owner, invitee, poll, invitation = invitation_data + invitation.delete() + client.force_login(owner) + + response = client.post( + reverse("invitations_send", args=[poll.url]), + {"invite": invitee.username}, + ) + + assert response.status_code == 302 + created = Invitation.objects.get(poll=poll, user=invitee) + assert created.creator == owner + diff --git a/bitpoll/invitations/tests/test_simple_get.py b/bitpoll/invitations/tests/test_simple_get.py new file mode 100644 index 00000000..daa3dcc7 --- /dev/null +++ b/bitpoll/invitations/tests/test_simple_get.py @@ -0,0 +1,46 @@ +import pytest + +from bitpoll.invitations.models import Invitation +from bitpoll.poll.models import Poll +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +URLS = get_module_urls("invitations") + + +@pytest.fixture +def invitation_data(db, django_user_model): + user = django_user_model.objects.create_user(username="invitation-user", password="password") + poll = Poll.objects.create(title="Invitation poll", url="invitation-poll", type="universal", user=user) + invitation = Invitation.objects.create( + date_created="2026-01-01T12:00:00Z", creator=user, user=user, poll=poll + ) + return user, poll, invitation + + +def url_args(poll): + return [poll.url] + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, invitation_data, url_name, arg_count): + _, poll, _ = invitation_data + response = client.get(get_dynamic_url(url_args(poll), url_name)) + match url_name: + case "invitations_send": + assert response.status_code == 405 + case _: + assert response.status_code == 200 + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, invitation_data, url_name, arg_count): + user, poll, _ = invitation_data + client.force_login(user) + response = client.get(get_dynamic_url(url_args(poll), url_name)) + match url_name: + case "invitations_send": + assert response.status_code == 405 + case _: + assert response.status_code == 200 + diff --git a/bitpoll/invitations/views.py b/bitpoll/invitations/views.py index cefc3a23..c5f00629 100644 --- a/bitpoll/invitations/views.py +++ b/bitpoll/invitations/views.py @@ -23,7 +23,7 @@ def invite(request, poll_url): error_msg = "Not allowed to edit" else: if 'resend_all' in request.POST: - for invitation in current_poll.invitation_set: + for invitation in current_poll.invitation_set.all(): invitation.send(request) return redirect('poll_settings', current_poll.url) @@ -55,7 +55,7 @@ def invitation_send(request, poll_url): request, _("You are not allowed to edit this Poll") ) else: - receivers = request.POST.get('invite', None).split() + receivers = request.POST.get('invite', '').split() for receiver in receivers: try: user = BitpollUser.objects.get(username=receiver) @@ -64,7 +64,7 @@ def invitation_send(request, poll_url): invitation.save() invitation.send(request) except IntegrityError: - messages.warning(request, _("The user {} was already invited".format(receiver))) + messages.warning(request, _("The user {} was already invited").format(receiver)) except ObjectDoesNotExist: try: group = Group.objects.get(name=receiver) @@ -86,6 +86,6 @@ def invitation_send(request, poll_url): user_error += " '{}'".format(receiver) if user_error: messages.error( - request, _("The following User/Groups could not be found: {}".format(user_error)) + request, _("The following User/Groups could not be found: {}").format(user_error) ) return redirect('invitations', current_poll.url) diff --git a/bitpoll/poll/admin.py b/bitpoll/poll/admin.py index 04f9d378..de5a9219 100644 --- a/bitpoll/poll/admin.py +++ b/bitpoll/poll/admin.py @@ -7,16 +7,36 @@ class PollAdmin(admin.ModelAdmin): list_display = ('title', 'url', 'user', 'created') search_fields = ('title', 'url', 'user__username') + raw_id_fields = ('user', 'group') @admin.register(Comment) class CommentAdmin(admin.ModelAdmin): list_display = ('name', 'poll', 'user', 'date_created') search_fields = ('text', 'name', 'user__username', 'poll__title') + raw_id_fields = ('poll', 'user') -admin.site.register(Choice) -admin.site.register(ChoiceValue) -admin.site.register(PollWatch) -admin.site.register(Vote) -admin.site.register(VoteChoice) +@admin.register(Choice) +class ChoiceAdmin(admin.ModelAdmin): + raw_id_fields = ('poll',) + + +@admin.register(ChoiceValue) +class ChoiceValueAdmin(admin.ModelAdmin): + raw_id_fields = ('poll',) + + +@admin.register(PollWatch) +class PollWatchAdmin(admin.ModelAdmin): + raw_id_fields = ('poll', 'user') + + +@admin.register(Vote) +class VoteAdmin(admin.ModelAdmin): + raw_id_fields = ('user', 'poll', 'assigned_by') + + +@admin.register(VoteChoice) +class VoteChoiceAdmin(admin.ModelAdmin): + raw_id_fields = ('value', 'vote', 'choice') diff --git a/bitpoll/poll/models.py b/bitpoll/poll/models.py index 8aa25bbb..4beed89a 100644 --- a/bitpoll/poll/models.py +++ b/bitpoll/poll/models.py @@ -42,8 +42,6 @@ class Poll(models.Model): created = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(BitpollUser, null=True, blank=True, on_delete=models.SET_NULL) group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) - """owner_id = models.ForeignKey(Member)""" - # === Extra settings == due_date = models.DateTimeField(null=True, blank=True) anonymous_allowed = models.BooleanField(default=True) @@ -95,10 +93,10 @@ def can_vote(self, user: AbstractUser, request: Optional[HttpRequest] = None, is return True def has_voted(self, user: AbstractUser) -> bool: - return user.is_authenticated and Vote.objects.filter(user=user, poll=self).count() > 0 + return user.is_authenticated and Vote.objects.filter(user=user, poll=self).exists() def get_own_vote(self, user: AbstractUser): - return Vote.objects.filter(user=user, poll=self)[0] + return Vote.objects.filter(user=user, poll=self).first() def can_edit(self, user: AbstractUser, request: HttpRequest=None) -> bool: """ @@ -112,7 +110,7 @@ def can_edit(self, user: AbstractUser, request: HttpRequest=None) -> bool: has_owner = self.group or self.user is_owner = self.is_owner(user) - can_edit = ((not has_owner) or is_owner) and user.is_authenticated or not has_owner + can_edit = ((not has_owner) or is_owner) and user.is_authenticated if request and not can_edit: messages.error( request, _("You are not allowed to edit this Poll.") @@ -120,11 +118,11 @@ def can_edit(self, user: AbstractUser, request: HttpRequest=None) -> bool: return can_edit def can_watch(self, user: AbstractUser) -> bool: + if self.is_owner(user): + return True if self.can_vote(user) and self.show_results not in ('never', 'summary after vote', 'complete after vote'): - # If the user can vote and the results are not restricted return True - if self.has_voted(user) and self.show_results not in ('never', ): - # If the user has voted and can view the results + if self.has_voted(user) and self.show_results not in ('never',): return True return False @@ -180,7 +178,6 @@ def fill(row, length): return matrix def get_tz_name(self, user: BitpollUser): - # TODO: local etc beachten (umrechenn auf user...) if self.type == 'date': # Datepolls are using UTC as timezone tz = 'UTC' @@ -206,7 +203,7 @@ def user_watches(self, user: BitpollUser) -> bool: :param user: The User in Question :return: True if the User watches the Poll """ - return user.is_authenticated and PollWatch.objects.filter(poll=self, user=user).count() > 0 + return user.is_authenticated and PollWatch.objects.filter(poll=self, user=user).exists() POLL_RESULT_SORTING = ( @@ -308,25 +305,15 @@ def can_edit(self, user: BitpollUser) -> bool: return False def can_delete(self, user: BitpollUser) -> bool: - """ - Determine if the user can delete the Vote - - :param user: - :return: - """ - #if self.poll.owner == user: # TODO: gruppen, owner algemein - # return True if user.is_anonymous: return False return self.can_edit(user) - + @property def display_name(self) -> str: - """Name of user if assigned, name field or anonymous else.""" if self.anonymous: return _('Anonymous') if self.user: - # return '{} ({})'.format(self.user.first_name, self.user.username) return self.user.get_displayname() return self.name @@ -359,7 +346,7 @@ def send(self, request, vote: Vote): translation.activate(self.user.language) link = reverse('poll', args=(self.poll.url,)) if vote.anonymous: - username = _("Annonymus") + username = _("Anonymous") elif vote.user: username = vote.user.username else: @@ -369,16 +356,16 @@ def send(self, request, vote: Vote): 'user': username if self.poll.show_results == "complete" else _("by an user"), 'poll': self.poll.title, 'link': link, - 'hide_participants': self.poll.hide_participants # TODO: simplify merge with usernameshadowing above + 'hide_participants': self.poll.hide_participants }) try: - send_mail(_("New votes for {}".format(self.poll.title)), email_content, None, + send_mail(_("New votes for {}").format(self.poll.title), email_content, None, [self.user.email]) except SMTPRecipientsRefused: translation.activate(old_lang) messages.error( - request, _("The mail server had an error sending the notification to {}".format( - self.user.username)) + request, _("The mail server had an error sending the notification to {}").format( + self.user.username) ) translation.activate(old_lang) diff --git a/bitpoll/poll/tests.py b/bitpoll/poll/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/bitpoll/poll/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/bitpoll/poll/tests/__init__.py b/bitpoll/poll/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/poll/tests/test_invitations.py b/bitpoll/poll/tests/test_invitations.py new file mode 100644 index 00000000..981250ca --- /dev/null +++ b/bitpoll/poll/tests/test_invitations.py @@ -0,0 +1,168 @@ +from django.test import TestCase, Client +from django.urls import reverse +from django.utils import timezone + +from bitpoll.base.models import BitpollUser +from bitpoll.poll.models import Poll, PollWatch, Vote + + +class CanWatchTestCase(TestCase): + """Tests for Poll.can_watch() logic.""" + + def setUp(self): + self.owner = BitpollUser.objects.create_user( + username='owner', password='testpass123' + ) + self.other_user = BitpollUser.objects.create_user( + username='other', password='testpass123' + ) + self.poll = Poll.objects.create( + title='Test Poll', + url='test-poll', + type='universal', + user=self.owner, + show_results='complete', + ) + + def test_owner_can_always_watch(self): + """Owner can watch regardless of show_results setting.""" + for results_setting, _ in Poll._meta.get_field('show_results').choices: + self.poll.show_results = results_setting + self.poll.save() + self.assertTrue( + self.poll.can_watch(self.owner), + f"Owner should be able to watch when show_results='{results_setting}'" + ) + + def test_user_can_watch_when_results_complete(self): + self.poll.show_results = 'complete' + self.poll.save() + self.assertTrue(self.poll.can_watch(self.other_user)) + + def test_user_can_watch_when_results_summary(self): + self.poll.show_results = 'summary' + self.poll.save() + self.assertTrue(self.poll.can_watch(self.other_user)) + + def test_user_cannot_watch_when_results_never(self): + self.poll.show_results = 'never' + self.poll.save() + self.assertFalse(self.poll.can_watch(self.other_user)) + + def test_user_cannot_watch_when_results_summary_after_vote(self): + self.poll.show_results = 'summary after vote' + self.poll.save() + self.assertFalse(self.poll.can_watch(self.other_user)) + + def test_user_cannot_watch_when_results_complete_after_vote(self): + self.poll.show_results = 'complete after vote' + self.poll.save() + self.assertFalse(self.poll.can_watch(self.other_user)) + + def test_voted_user_can_watch_summary_after_vote(self): + self.poll.show_results = 'summary after vote' + self.poll.save() + Vote.objects.create( + name='other', user=self.other_user, poll=self.poll, + date_created=timezone.now(), comment='', + ) + self.assertTrue(self.poll.can_watch(self.other_user)) + + def test_voted_user_cannot_watch_when_never(self): + self.poll.show_results = 'never' + self.poll.save() + Vote.objects.create( + name='other', user=self.other_user, poll=self.poll, + date_created=timezone.now(), comment='', + ) + self.assertFalse(self.poll.can_watch(self.other_user)) + + +class WatchViewTestCase(TestCase): + """Tests for the watch/unwatch view.""" + + def setUp(self): + self.owner = BitpollUser.objects.create_user( + username='owner', password='testpass123' + ) + self.other_user = BitpollUser.objects.create_user( + username='other', password='testpass123' + ) + self.poll = Poll.objects.create( + title='Test Poll', + url='test-poll', + type='universal', + user=self.owner, + show_results='complete', + ) + self.watch_url = reverse('poll_watch', args=['test-poll']) + self.client = Client() + + def test_watch_requires_login(self): + response = self.client.post(self.watch_url) + self.assertEqual(response.status_code, 302) + self.assertIn('login', response.url) + + def test_watch_requires_post(self): + self.client.login(username='owner', password='testpass123') + response = self.client.get(self.watch_url) + self.assertEqual(response.status_code, 405) + + def test_user_can_watch_poll(self): + self.client.login(username='other', password='testpass123') + self.client.post(self.watch_url) + self.assertTrue( + PollWatch.objects.filter(poll=self.poll, user=self.other_user).exists() + ) + + def test_user_can_unwatch_poll(self): + PollWatch.objects.create(poll=self.poll, user=self.other_user) + self.client.login(username='other', password='testpass123') + self.client.post(self.watch_url) + self.assertFalse( + PollWatch.objects.filter(poll=self.poll, user=self.other_user).exists() + ) + + def test_unwatch_allowed_when_results_changed_to_never(self): + """The main bug: user watches, then results set to 'never', unwatch must still work.""" + PollWatch.objects.create(poll=self.poll, user=self.other_user) + self.poll.show_results = 'never' + self.poll.save() + + self.client.login(username='other', password='testpass123') + self.client.post(self.watch_url) + self.assertFalse( + PollWatch.objects.filter(poll=self.poll, user=self.other_user).exists() + ) + + def test_watch_denied_when_results_never(self): + """User cannot subscribe to watch when results are 'never'.""" + self.poll.show_results = 'never' + self.poll.save() + + self.client.login(username='other', password='testpass123') + self.client.post(self.watch_url) + self.assertFalse( + PollWatch.objects.filter(poll=self.poll, user=self.other_user).exists() + ) + + def test_owner_can_watch_when_results_never(self): + self.poll.show_results = 'never' + self.poll.save() + + self.client.login(username='owner', password='testpass123') + self.client.post(self.watch_url) + self.assertTrue( + PollWatch.objects.filter(poll=self.poll, user=self.owner).exists() + ) + + def test_owner_can_unwatch_when_results_never(self): + PollWatch.objects.create(poll=self.poll, user=self.owner) + self.poll.show_results = 'never' + self.poll.save() + + self.client.login(username='owner', password='testpass123') + self.client.post(self.watch_url) + self.assertFalse( + PollWatch.objects.filter(poll=self.poll, user=self.owner).exists() + ) diff --git a/bitpoll/poll/tests/test_post.py b/bitpoll/poll/tests/test_post.py new file mode 100644 index 00000000..65ac6ca6 --- /dev/null +++ b/bitpoll/poll/tests/test_post.py @@ -0,0 +1,341 @@ +import pytest +from faker import Faker +from model_bakery import baker +from django.forms.models import model_to_dict +from django.urls import reverse +from django.utils.timezone import now + +from bitpoll.poll.models import Choice, ChoiceValue, Comment, Poll, PollWatch, Vote, VoteChoice + + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def owner(): + return baker.make( + "base.BitpollUser", + _fill_optional=True, + username="poll-owner", + password="password", + is_active=True, + ) + + +@pytest.fixture +def user(): + return baker.make( + "base.BitpollUser", + _fill_optional=True, + username="poll-user", + password="password", + is_active=True, + ) + + +@pytest.fixture +def poll(owner): + return baker.make( + Poll, + _fill_optional=True, + title="Test poll", + url="test-poll", + type="universal", + user=owner, + anonymous_allowed=True, + require_login=False, + require_invitation=False, + allow_unauthenticated_vote_changes=True, + one_vote_per_user=True, + vote_all=False, + due_date=None, + ) + + +@pytest.fixture +def choice(poll): + return baker.make( + Choice, + _fill_optional=True, + poll=poll, + text="Original choice", + sort_key=0, + deleted=False, + ) + + +@pytest.fixture +def value(poll): + return baker.make( + ChoiceValue, + _fill_optional=True, + poll=poll, + title="Yes", + icon="check", + color="00ff00", + deleted=False, + ) + + +def post_url(name, poll, *args): + return reverse(name, args=[poll.url, *args]) + + +def test_comment_post_creates_comment(client, poll, user): + client.force_login(user) + comment = baker.make(Comment, _fill_optional=True, poll=poll, user=user) + data = model_to_dict(comment, fields=["name", "text"]) + data["text"] = "A new comment" + + response = client.post(post_url("poll_comment", poll), data) + + assert response.status_code == 302 + comment = Comment.objects.exclude(pk=comment.pk).get(poll=poll) + assert comment.text == "A new comment" + assert comment.user == user + assert comment.name == user.get_displayname() + + +def test_comment_edit_post_updates_comment(client, poll, user): + comment = baker.make(Comment, _fill_optional=True, poll=poll, user=user, text="Old") + client.force_login(user) + data = model_to_dict(comment, fields=["name", "text"]) + data["text"] = Faker().sentence() + + response = client.post(post_url("poll_comment_edit", poll, comment.pk), data) + + assert response.status_code == 302 + comment.refresh_from_db() + assert comment.text == data["text"] + + +def test_watch_post_toggles_watch(client, poll, user): + client.force_login(user) + url = post_url("poll_watch", poll) + + assert client.post(url).status_code == 302 + assert PollWatch.objects.filter(poll=poll, user=user).exists() + + assert client.post(url).status_code == 302 + assert not PollWatch.objects.filter(poll=poll, user=user).exists() + + +def test_universal_choice_post_creates_and_updates_choices(client, poll, choice): + client.force_login(poll.user) + data = { + "choice_text": ["Changed choice", "New choice"], + "choice_sort_key": [str(choice.pk), "1"], + **{ + f"choice_text_{choice.pk}": model_to_dict(choice)["text"], + f"choice_sort_key_{choice.pk}": str(choice.sort_key), + }, + "next": "Poll", + } + data[f"choice_text_{choice.pk}"] = Faker().sentence()[:80] + + response = client.post(post_url("poll_editUniversalChoice", poll), data) + + assert response.status_code == 302 + choice.refresh_from_db() + assert choice.text == data[f"choice_text_{choice.pk}"] + assert Choice.objects.filter(poll=poll, text="New choice", deleted=False).exists() + + +def test_universal_choice_post_soft_deletes_choice(client, poll, choice): + client.force_login(poll.user) + + response = client.post( + post_url("poll_editUniversalChoice", poll), + {f"choice_text_{choice.pk}": "", "delete": str(choice.pk)}, + ) + + assert response.status_code == 200 + choice.refresh_from_db() + assert choice.deleted is True + + +def test_date_choice_post_replaces_choices(client, owner): + poll = baker.make(Poll, user=owner, type="date", url="date-poll") + old_choice = baker.make(Choice, poll=poll, date=now(), text="", sort_key=0) + client.force_login(owner) + + response = client.post( + post_url("poll_editDateChoice", poll), + {"dates": "2026-08-10,2026-08-11"}, + ) + + assert response.status_code == 302 + old_choice.refresh_from_db() + assert old_choice.deleted is True + assert Choice.objects.filter(poll=poll, deleted=False).count() == 2 + + +def test_datetime_combinations_post_creates_choices(client, owner): + poll = baker.make(Poll, user=owner, type="datetime", url="datetime-poll") + client.force_login(owner) + + response = client.post( + post_url("poll_editDTChoiceCombinations", poll), + {"datetimes[]": ["2026-08-10 10:00", "2026-08-10 11:00"]}, + ) + + assert response.status_code == 302 + assert Choice.objects.filter(poll=poll, deleted=False).count() == 2 + + +def test_choice_value_create_post_creates_value(client, poll): + client.force_login(poll.user) + choice_value = baker.make(ChoiceValue, _fill_optional=True, poll=poll) + data = model_to_dict(choice_value, fields=["title", "icon", "color", "weight"]) + data.update(title="Maybe", color="#abcdef") + + response = client.post(post_url("poll_editchoicevalues_create", poll), data) + + assert response.status_code == 302 + created = ChoiceValue.objects.get(poll=poll, title="Maybe") + assert created.color == "abcdef" + + +def test_vote_post_creates_vote_and_votechoice(client, poll, choice, value): + vote = baker.make(Vote, _fill_optional=True, poll=poll) + data = model_to_dict(vote, fields=["comment"]) + vote.delete() + data.update( + { + str(choice.pk): str(value.pk), + f"comment_{choice.pk}": "Vote comment", + "name": "", + "anonymous": "true", + } + ) + data["comment"] = "" + + response = client.post(post_url("poll_vote", poll), data) + + assert response.status_code == 302 + vote = Vote.objects.get(poll=poll) + assert vote.user is None + assert vote.anonymous is True + assert VoteChoice.objects.filter(vote=vote, choice=choice, value=value, comment="Vote comment").exists() + + +def test_vote_edit_post_replaces_votechoices(client, poll, choice, value, user): + vote = baker.make(Vote, _fill_optional=True, poll=poll, user=user, comment="Old") + old_choice = baker.make(Choice, _fill_optional=True, poll=poll, sort_key=1) + old_value = baker.make(ChoiceValue, _fill_optional=True, poll=poll) + baker.make( + VoteChoice, + _fill_optional=True, + vote=vote, + choice=old_choice, + value=old_value, + ) + client.force_login(user) + data = model_to_dict(vote, fields=["comment"]) + data.update({"vote_id": str(vote.pk), str(choice.pk): str(value.pk), f"comment_{choice.pk}": "New choice comment"}) + + data["comment"] = Faker().sentence() + response = client.post(post_url("poll_voteEdit", poll, vote.pk), data) + + assert response.status_code == 302 + vote.refresh_from_db() + assert vote.comment == data["comment"] + assert VoteChoice.objects.filter(vote=vote, choice=choice, value=value).exists() + assert not VoteChoice.objects.filter(vote=vote, choice=old_choice).exists() + + +def test_vote_assign_post_assigns_user(client, poll, owner, user): + vote = baker.make(Vote, _fill_optional=True, poll=poll, name="Anonymous") + client.force_login(owner) + + response = client.post(post_url("poll_voteAssign", poll, vote.pk), {"username": user.username}) + + assert response.status_code == 302 + vote.refresh_from_db() + assert vote.user == user + assert vote.assigned_by == owner + + +def test_vote_delete_post_deletes_vote(client, poll, user): + vote = baker.make(Vote, _fill_optional=True, poll=poll, user=user) + client.force_login(user) + + response = client.post(post_url("poll_voteDelete", poll, vote.pk), {"Delete": "Delete"}) + + assert response.status_code == 302 + assert not Vote.objects.filter(pk=vote.pk).exists() + + +def test_settings_post_updates_poll(client, poll): + client.force_login(poll.user) + data = { + "title": "Updated title", + "due_date": "", + "show_results": "summary", + "timezone_name": "UTC", + "description": "Updated description", + "allow_comments": "on", + "anonymous_allowed": "on", + "require_login": "", + "require_invitation": "", + "allow_unauthenticated_vote_changes": "on", + "one_vote_per_user": "on", + "show_invitations": "on", + "group": "", + "public_listening": "", + "vote_all": "", + "hide_participants": "", + "use_user_timezone": "", + "sorting": "0", + "show_score_in_summary": "", + "user": poll.user.username, + } + + response = client.post(post_url("poll_settings", poll), data) + + assert response.status_code == 302 + poll.refresh_from_db() + assert poll.title == "Updated title" + assert poll.show_results == "summary" + assert poll.timezone_name == "UTC" + + +def test_copy_post_copies_choices_and_values(client, poll, choice, value): + client.force_login(poll.user) + + response = client.post( + post_url("poll_copy", poll), + { + "title": "Copied poll", + "url": "copied-poll", + "due_date": "", + "copy_choices": "on", + "copy_ans_values": "on", + "date_shift": "0", + }, + ) + + assert response.status_code == 302 + copied = Poll.objects.get(url="copied-poll") + assert copied.title == "Copied poll" + assert Choice.objects.filter(poll=copied, text=choice.text).exists() + assert ChoiceValue.objects.filter(poll=copied, title=value.title).exists() + + +def test_poll_delete_post_deletes_poll(client, poll): + client.force_login(poll.user) + + response = client.post(post_url("poll_delete", poll), {"Delete": "Delete"}) + + assert response.status_code == 302 + assert not Poll.objects.filter(pk=poll.pk).exists() + + + + + + + + + + diff --git a/bitpoll/poll/tests/test_simple_get.py b/bitpoll/poll/tests/test_simple_get.py new file mode 100644 index 00000000..39ffc18a --- /dev/null +++ b/bitpoll/poll/tests/test_simple_get.py @@ -0,0 +1,78 @@ +import pytest +from django.utils.timezone import now + +from bitpoll.poll.models import Choice, ChoiceValue, Comment, Poll, Vote, VoteChoice +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +URLS = get_module_urls("poll") + + +@pytest.fixture +def poll(db, django_user_model): + user = django_user_model.objects.create_user(username="poll-owner", password="password") + return Poll.objects.create(title="Test poll", url="test-poll", type="universal", user=user) + + +@pytest.fixture +def comment(poll): + return Comment.objects.create( + text="Test comment", + date_created=now(), + name="Poll owner", + user=poll.user, + poll=poll, + ) + + +@pytest.fixture +def vote(poll): + choice = Choice.objects.create(text="Test choice", poll=poll, sort_key=0) + choice_value = ChoiceValue.objects.create( + title="Yes", icon="check", color="00ff00", poll=poll + ) + vote = Vote.objects.create( + name="Test voter", + date_created=now(), + comment="", + poll=poll, + ) + VoteChoice.objects.create(comment="", value=choice_value, vote=vote, choice=choice) + return vote + + +def url_args(url_name, poll, comment, vote): + if url_name in {"poll_comment_edit", "poll_deleteComment"}: + return [poll.url, comment.pk] + if url_name in {"poll_voteAssign", "poll_voteEdit", "poll_voteDelete"}: + return [poll.url, vote.pk] + return [poll.url] + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, poll, comment, vote, url_name, arg_count): + response = client.get(get_dynamic_url(url_args(url_name, poll, comment, vote), url_name)) + match url_name: + case "poll_comment_edit": + assert response.status_code == 302 + case "poll_watch" | "poll_editchoicevalues_create": + assert response.status_code == 405 + case "poll_settings" | "poll_editChoice" | "poll_editDateChoice" | "poll_editDTChoiceDate" | "poll_editDTChoiceTime" | "poll_editDTChoiceCombinations" | "poll_editUniversalChoice" | "poll_editchoicevalues" | "poll_copy": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, poll, comment, vote, url_name, arg_count): + client.force_login(poll.user) + response = client.get(get_dynamic_url(url_args(url_name, poll, comment, vote), url_name)) + match url_name: + case "poll_watch" | "poll_editchoicevalues_create": + assert response.status_code == 405 + case "poll_editChoice" | "poll_editDTChoiceTime" | "poll_editDTChoiceCombinations": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + diff --git a/bitpoll/poll/urls.py b/bitpoll/poll/urls.py index 6c9609ec..bc16145b 100644 --- a/bitpoll/poll/urls.py +++ b/bitpoll/poll/urls.py @@ -4,7 +4,7 @@ urlpatterns = [ re_path(r'^([a-zA-Z0-9_\-]+)/$', views.poll, name='poll'), - re_path(r'^([a-zA-Z0-9_\-]+).csv$', views.poll, {'export': True}, name='poll_export_csv'), + re_path(r'^([a-zA-Z0-9_\-]+)\.csv$', views.poll, {'export': True}, name='poll_export_csv'), re_path(r'^([a-zA-Z0-9_\-]+)/comment/$', views.comment, name='poll_comment'), re_path(r'^([a-zA-Z0-9_\-]+)/comment/(\d+)/edit/$', views.comment, name='poll_comment_edit'), re_path(r'^([a-zA-Z0-9_\-]+)/comment/(\d+)/delete/$', views.delete_comment, name='poll_deleteComment'), @@ -18,8 +18,8 @@ re_path(r'^([a-zA-Z0-9_\-]+)/edit/choices/dateTime/combinations/$', views.edit_dt_choice_combinations, name='poll_editDTChoiceCombinations'), re_path(r'^([a-zA-Z0-9_\-]+)/edit/choices/universal/$', views.edit_universal_choice, name='poll_editUniversalChoice'), - re_path(r'^([a-zA-Z0-9_\-]+)/edit/choicevalues/', views.edit_choicevalues, name='poll_editchoicevalues'), - re_path(r'^([a-zA-Z0-9_\-]+)/edit/choicevalues_create', views.edit_choicevalues_create, + re_path(r'^([a-zA-Z0-9_\-]+)/edit/choicevalues/$', views.edit_choicevalues, name='poll_editchoicevalues'), + re_path(r'^([a-zA-Z0-9_\-]+)/edit/choicevalues_create$', views.edit_choicevalues_create, name='poll_editchoicevalues_create'), re_path(r'^([a-zA-Z0-9_\-]+)/delete/$', views.delete, name='poll_delete'), diff --git a/bitpoll/poll/views.py b/bitpoll/poll/views.py index c6edd0b5..811226e9 100644 --- a/bitpoll/poll/views.py +++ b/bitpoll/poll/views.py @@ -135,11 +135,11 @@ def poll(request, poll_url: str, export: bool=False): # warn the user if the Timezone is not the same on the Poll and in his settings different_timezone = current_poll.timezone_name != request.user.timezone if current_poll.use_user_timezone and different_timezone: - messages.info(request, _("This poll was transferred from {} to your local timezone {}".format( - current_poll.timezone_name, request.user.timezone))) + messages.info(request, _("This poll was transferred from {} to your local timezone {}").format( + current_poll.timezone_name, request.user.timezone)) elif different_timezone: - messages.warning(request, _("This poll has a different timezone ({}) than you.".format( - current_poll.timezone_name))) + messages.warning(request, _("This poll has a different timezone ({}) than you.").format( + current_poll.timezone_name)) deleted_choicevals_count = VoteChoice.objects.filter(choice__poll=current_poll, value__deleted=True).count() if deleted_choicevals_count > 0: @@ -307,7 +307,7 @@ def delete_comment(request, poll_url, comment_id): current_comment.delete() return redirect('poll', poll_url) else: - error_msg = _("Deletion not allowed. You are not {}.".format(str(current_comment.name))) + error_msg = _("Deletion not allowed. You are not {}.").format(str(current_comment.name)) else: error_msg = _("Deletion not allowed. You are not authenticated.") else: @@ -325,16 +325,15 @@ def delete_comment(request, poll_url, comment_id): def watch(request, poll_url): current_poll = get_object_or_404(Poll, url=poll_url) - if not current_poll.can_watch(request.user): - messages.error( - request, _("You are not allowed to watch this poll.") - ) - return redirect('poll', poll_url) - if current_poll.user_watches(request.user): poll_watch = PollWatch.objects.get(poll=current_poll, user=request.user) poll_watch.delete() else: + if not current_poll.can_watch(request.user): + messages.error( + request, _("You are not allowed to watch this poll.") + ) + return redirect('poll', poll_url) poll_watch = PollWatch(poll=current_poll, user=request.user) poll_watch.save() return redirect('poll', poll_url) @@ -394,7 +393,7 @@ def edit_date_choice(request, poll_url): dates.append(date) else: error = True - messages.error(_("There was en error interpreting the provided dates and times")) + messages.error(request, _("There was an error interpreting the provided dates and times")) except ValueError: # This will most likely only happen with users turning of JS error = True @@ -820,7 +819,7 @@ def vote(request, poll_url, vote_id=None): except IntegrityError: pass else: - current_vote.name = request.POST.get('name').strip() + current_vote.name = request.POST.get('name', '').strip() if len(current_vote.name) > 80: messages.error( @@ -951,16 +950,16 @@ def vote_assign(request, poll_url, vote_id): username = request.POST.get('username').strip() try: user = BitpollUser.objects.get(username=username) - if not current_poll.vote_set.filter(Q(user=user)) and current_poll.one_vote_per_user: + if current_poll.vote_set.filter(Q(user=user)).exists() and current_poll.one_vote_per_user: + messages.info(request, _("This user has already voted and only one vote per user is permitted")) + else: current_vote.user = user current_vote.name = user.get_displayname() current_vote.assigned_by = request.user current_vote.save() return redirect('poll', poll_url) - else: - messages.info(request, _("This user has already voted and only one vote per user is permitted")) except ObjectDoesNotExist: - messages.warning(request, _("The user {} does not exists".format(username))) + messages.warning(request, _("The user {} does not exist").format(username)) else: return HttpResponseForbidden() @@ -1002,7 +1001,7 @@ def vote_delete(request, poll_url, vote_id): current_vote.delete() return redirect('poll', poll_url) else: - error_msg = _("Deletion not allowed. You are not {}.".format(str(current_vote.name))) + error_msg = _("Deletion not allowed. You are not {}.").format(str(current_vote.name)) else: error_msg = _("Deletion not allowed. You are not authenticated.") else: @@ -1141,10 +1140,9 @@ def settings(request, poll_url): user_obj = BitpollUser.objects.get(username=user) new_poll.user = user_obj except ObjectDoesNotExist: - user_error = _("User {} not Found".format(user)) + user_error = _("User {} not Found").format(user) else: new_poll.user = None - current_poll.choice_set.all() if not user_error: # change the Timezone in the Choices, date-polls are in UTC regardles of the timezone if old_timezone_name != new_poll.timezone_name and current_poll.type == 'datetime': diff --git a/bitpoll/registration/forms.py b/bitpoll/registration/forms.py index 462a295e..9f147328 100644 --- a/bitpoll/registration/forms.py +++ b/bitpoll/registration/forms.py @@ -57,10 +57,9 @@ def __init__(self, user, *args, **kwargs): nickname = forms.CharField(max_length=20) def save(self): - ldap_user = self.user.get_ldapuser() - ldap_user.display_name = u"{} ({})".format( + self.user.displayname = u"{} ({})".format( self.cleaned_data['nickname'], self.user.username) - ldap_user.save() + self.user.save() class EmailChangeForm(forms.Form): diff --git a/bitpoll/registration/tests/__init__.py b/bitpoll/registration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/registration/tests/test_post.py b/bitpoll/registration/tests/test_post.py new file mode 100644 index 00000000..501a79c8 --- /dev/null +++ b/bitpoll/registration/tests/test_post.py @@ -0,0 +1,61 @@ +import pytest +from model_bakery import baker +from django.core import signing +from django.urls import reverse + +from bitpoll.base.models import BitpollUser + + +pytestmark = pytest.mark.django_db + + +def test_request_account_post_sends_activation_email(client): + data = { + "username": "new-registration-user", + "first_name": "New", + "last_name": "User", + "email": "new-user@example.com", + "auto_watch": "", + "email_invitation": "on", + } + + response = client.post(reverse("registration_request_account"), data) + + assert response.status_code == 302 + assert response.url.endswith("/new-user@example.com") + + +def test_create_account_post_creates_user(client): + token = signing.dumps( + { + "username": "created-user", + "first_name": "Created", + "last_name": "User", + "email": "created@example.com", + "email_invitation": True, + } + ) + + response = client.post( + reverse("registration_create_account", args=[token]), + {"new_password1": "SafePassword123!", "new_password2": "SafePassword123!"}, + ) + + assert response.status_code == 302 + user = BitpollUser.objects.get(username="created-user") + assert user.check_password("SafePassword123!") + + +def test_account_nickname_post_updates_user(client): + user = baker.make("base.BitpollUser", _fill_optional=True, username="nickname-user") + client.force_login(user) + + response = client.post( + reverse("registration_account"), + {"form": "change_nick", "nickname": "New Nickname"}, + ) + + assert response.status_code == 302 + user.refresh_from_db() + assert user.displayname == "New Nickname (nickname-user)" + diff --git a/bitpoll/registration/tests/test_simple_get.py b/bitpoll/registration/tests/test_simple_get.py new file mode 100644 index 00000000..74508203 --- /dev/null +++ b/bitpoll/registration/tests/test_simple_get.py @@ -0,0 +1,40 @@ +import pytest + +from bitpoll.tests._base import get_dynamic_url, get_module_urls + + +URLS = get_module_urls("registration") + + +def url_args(url_name): + if "successful" in url_name: + return ["user@example.com"] + if "change_email" in url_name or "create_account" in url_name: + return ["invalid-token"] + if "confirm" in url_name: + return ["1", "invalid-token"] + return [] + + +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_without_login(client, url_name, arg_count): + response = client.get(get_dynamic_url(url_args(url_name), url_name)) + match url_name: + case "registration_change_email" | "registration_account": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + + +@pytest.mark.django_db +@pytest.mark.parametrize("url_name,arg_count", URLS) +def test_urls_with_login(client, django_user_model, url_name, arg_count): + user = django_user_model.objects.create_user(username="registration-user", password="password") + client.force_login(user) + response = client.get(get_dynamic_url(url_args(url_name), url_name)) + match url_name: + case "registration_create_account": + assert response.status_code == 302 + case _: + assert response.status_code == 200 + diff --git a/bitpoll/registration/urls.py b/bitpoll/registration/urls.py index 5af073c0..2e476b3e 100644 --- a/bitpoll/registration/urls.py +++ b/bitpoll/registration/urls.py @@ -1,7 +1,7 @@ from django.urls import path, re_path from django.contrib.auth import views as auth_views from django.contrib.auth.forms import PasswordResetForm -from django.urls import path +from django.conf import settings from .views import * @@ -17,13 +17,13 @@ # TODO: own settings value for changing password? If yes: remember to change template ifs when to show the link if settings.REGISTER_ENABLED: urlpatterns += [ - path('password_reset/', auth_views.PasswordResetView.as_view(), { - 'password_reset_form': PasswordResetForm - }, name='password_reset'), - path('password_reset/done', auth_views.PasswordResetDoneView.as_view(), + path('password_reset/', auth_views.PasswordResetView.as_view( + form_class=PasswordResetForm + ), name='password_reset'), + path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), path('reset///', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), - re_path(r'^password_reset/complete', auth_views.PasswordResetCompleteView.as_view(), + re_path(r'^password_reset/complete$', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete') ] diff --git a/bitpoll/registration/views.py b/bitpoll/registration/views.py index fcf041a6..bfa68a7e 100644 --- a/bitpoll/registration/views.py +++ b/bitpoll/registration/views.py @@ -182,13 +182,13 @@ def _send_mail_or_error_page(subject, content, address, request): try: send_mail(subject, content, None, [address]) except SMTPRecipientsRefused as e: - wrong_email, (error_code, error_msg) = e.recipients.items()[0] + wrong_email, (error_code, error_msg) = next(iter(e.recipients.items())) unknown = 'User unknown' in error_msg if not unknown: error_email_content = u'{0}: {1}'.format(e.__class__.__name__, repr(e.recipients)) send_mail( - _('Registration: Sending mail failed: {}'.format(address)), + _('Registration: Sending mail failed: {}').format(address), error_email_content, None, [settings.TEAM_EMAIL]) diff --git a/bitpoll/settings.py b/bitpoll/settings.py index 2b7ccd4b..8b468b9a 100644 --- a/bitpoll/settings.py +++ b/bitpoll/settings.py @@ -18,6 +18,13 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +# Overridden by settings_local.py in deployments. These defaults keep Django +# importable for local development and automated tests. +SECRET_KEY = 'bitpoll-development-only-secret-key' +FIELD_ENCRYPTION_KEY = 'this+is+an+example+key+please+generate+one+=' +DEBUG = False +ALLOWED_HOSTS = ['testserver', 'localhost', '127.0.0.1'] + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ @@ -285,6 +292,8 @@ AUTH_USER_MODEL = 'base.BitpollUser' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + CSRF_COOKIE_HTTPONLY = True DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 @@ -298,8 +307,6 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True @@ -414,11 +421,20 @@ OPENID_ENABLED = False -from .settings_local import * +# Local settings are intentionally ignored by git and are optional for tests +# and development. Production deployments can still override every setting. +INSTALLED_APPS_LOCAL = [] +MIDDLEWARE_LOCAL = [] +PIPELINE_LOCAL = {} + +try: + from .settings_local import * +except ModuleNotFoundError as error: + if error.name != 'bitpoll.settings_local': + raise INSTALLED_APPS += INSTALLED_APPS_LOCAL -if "MIDDLEWARE_LOCAL" in locals(): - MIDDLEWARE += MIDDLEWARE_LOCAL +MIDDLEWARE += MIDDLEWARE_LOCAL PIPELINE.update(PIPELINE_LOCAL) if OPENID_ENABLED: diff --git a/bitpoll/settings_test.py b/bitpoll/settings_test.py new file mode 100644 index 00000000..48bad8f0 --- /dev/null +++ b/bitpoll/settings_test.py @@ -0,0 +1,18 @@ +"""Django settings used by the pytest suite. + +The regular settings remain the source of truth; this module only disables +production-only static manifest behavior and external integrations. +""" + +from .settings import * # noqa: F401,F403 + +DEBUG = True +ALLOWED_HOSTS = ["testserver", "localhost", "127.0.0.1"] +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" +STORAGES = { + **STORAGES, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} + diff --git a/bitpoll/tests/__init__.py b/bitpoll/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitpoll/tests/_base.py b/bitpoll/tests/_base.py new file mode 100644 index 00000000..1f5eafc8 --- /dev/null +++ b/bitpoll/tests/_base.py @@ -0,0 +1,27 @@ +import re + +from django.urls import URLPattern, URLResolver, get_resolver, reverse + + +def extract_patterns(url_patterns, args=None): + """Yield named URL patterns and positional regex arguments recursively.""" + args = [] if args is None else list(args) + for pattern in url_patterns: + pattern_args = args + re.findall(r"\((?!\?P<)[^)]*\)", str(pattern.pattern)) + if isinstance(pattern, URLPattern): + if pattern.name: + yield pattern, pattern_args + elif isinstance(pattern, URLResolver): + yield from extract_patterns(pattern.url_patterns, pattern_args) + + +def get_module_urls(module): + """Return ``(url_name, argument_count)`` for every named module URL.""" + resolver = get_resolver(f"bitpoll.{module}.urls") + return [(pattern.name, len(args)) for pattern, args in extract_patterns(resolver.url_patterns)] + + +def get_dynamic_url(args, url_name): + """Reverse a Bitpoll URL using positional values for its regex groups.""" + return reverse(url_name, args=args) + diff --git a/bitpoll/urls.py b/bitpoll/urls.py index 9b39412c..47c19084 100644 --- a/bitpoll/urls.py +++ b/bitpoll/urls.py @@ -19,7 +19,6 @@ from django.urls import include, path, re_path from django.contrib import admin from django.shortcuts import redirect, render -from django.urls import path from django.views.generic.base import RedirectView import django.conf.urls.i18n @@ -29,10 +28,10 @@ path('poll/', include('bitpoll.poll.urls')), path('', include('bitpoll.base.urls')), path('invitations/', include('bitpoll.invitations.urls')), - path(r'registration/', include('bitpoll.registration.urls')), + path('registration/', include('bitpoll.registration.urls')), - path(r'i18n/', include(django.conf.urls.i18n)), - path(r'admin/', admin.site.urls), + path('i18n/', include(django.conf.urls.i18n)), + path('admin/', admin.site.urls), ] if settings.OPENID_ENABLED and settings.GROUP_MANAGEMENT: diff --git a/docker_files/uwsgi-bitpoll.ini b/docker_files/uwsgi-bitpoll.ini index a86a865c..859c2f76 100644 --- a/docker_files/uwsgi-bitpoll.ini +++ b/docker_files/uwsgi-bitpoll.ini @@ -7,7 +7,7 @@ http-socket = :3009 plugins = python3 chdir = /opt/bitpoll -virtualenv = /usr/local +virtualenv = /opt/bitpoll/.venv module = bitpoll.wsgi:application env = DJANGO_SETTINGS_MODULE=bitpoll.settings diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..19a4998e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[project] +name = "bitpoll" +version = "0.0.0" +description = "A Django application for date and time polls" +readme = "README.md" +requires-python = ">=3.12,<3.14" +dependencies = [ + "caldav", + "django", + "django-encrypted-model-fields", + "django-friendly-tag-loader", + "django-markdownify", + "django-pipeline", + "django-simple-csp", + "django-token-bucket>=0.2.2", + "django-widget-tweaks", + "icalendar", + "libsasscompiler", + "pytz", + "urllib3", +] + +[project.optional-dependencies] +production = [ + "django-auth-ldap", + "psycopg2-binary", + "sentry-sdk", + "simple-openid-connect[django]", + "uwsgi", +] + +dev = [ + "faker", + "model-bakery", + "pytest", + "pytest-django", + "pytest-cov", +] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "bitpoll.settings_test" +python_files = ["test_*.py", "*_test.py"] +testpaths = ["bitpoll"] +addopts = "--strict-markers" +markers = [ + "django_db: test requires access to the Django test database", +] + +[tool.uv] +package = false + +[dependency-groups] +dev = [ + "faker", + "model-bakery", + "pytest", + "pytest-cov", + "pytest-django", +] + + + + + + + diff --git a/requirements-production.in b/requirements-production.in deleted file mode 100644 index 9d847ba2..00000000 --- a/requirements-production.in +++ /dev/null @@ -1,5 +0,0 @@ -sentry-sdk -django-auth-ldap -psycopg2-binary -simple_openid_connect[django] -uwsgi diff --git a/requirements-production.txt b/requirements-production.txt deleted file mode 100644 index 1ddcf98b..00000000 --- a/requirements-production.txt +++ /dev/null @@ -1,167 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.14 -# by the following command: -# -# pip-compile --output-file=requirements-production.txt requirements-production.in requirements.in -# -annotated-types==0.7.0 - # via pydantic -asgiref==3.11.1 - # via django -bleach[css]==6.4.0 - # via django-markdownify -caldav==3.2.0 - # via -r requirements.in -certifi==2026.4.22 - # via - # requests - # sentry-sdk -cffi==2.0.0 - # via cryptography -charset-normalizer==3.4.7 - # via - # niquests - # requests -click==8.3.3 - # via x-wr-timezone -cryptography==48.0.1 - # via - # cryptojwt - # django-encrypted-model-fields -cryptojwt==1.11.0 - # via simple-openid-connect -django==6.0.5 - # via - # -r requirements.in - # django-auth-ldap - # django-encrypted-model-fields - # django-markdownify - # django-simple-csp - # django-token-bucket - # django-widget-tweaks - # simple-openid-connect -django-auth-ldap==5.3.0 - # via -r requirements-production.in -django-encrypted-model-fields==0.6.5 - # via -r requirements.in -django-friendly-tag-loader==1.3.2 - # via -r requirements.in -django-markdownify==0.9.6 - # via -r requirements.in -django-pipeline==3.1.0 - # via - # -r requirements.in - # libsasscompiler -django-simple-csp==0.5.dev1 - # via -r requirements.in -django-token-bucket==0.2.4 - # via -r requirements.in -django-widget-tweaks==1.5.1 - # via -r requirements.in -dnspython==2.8.0 - # via caldav -furl==2.1.4 - # via simple-openid-connect -h11==0.16.0 - # via urllib3-future -icalendar==7.1.0 - # via - # -r requirements.in - # caldav - # icalendar-searcher - # recurring-ical-events - # x-wr-timezone -icalendar-searcher==1.0.5 - # via caldav -idna==3.14 - # via requests -jh2==5.0.11 - # via urllib3-future -libsass==0.23.0 - # via libsasscompiler -libsasscompiler==0.2.1 - # via -r requirements.in -lxml==6.1.0 - # via caldav -markdown==3.10.2 - # via django-markdownify -niquests==3.18.8 - # via caldav -orderedmultidict==1.0.2 - # via furl -psycopg2-binary==2.9.12 - # via -r requirements-production.in -pyasn1==0.6.3 - # via - # pyasn1-modules - # python-ldap -pyasn1-modules==0.4.2 - # via python-ldap -pycparser==3.0 - # via cffi -pydantic==2.13.4 - # via simple-openid-connect -pydantic-core==2.46.4 - # via pydantic -python-dateutil==2.9.0.post0 - # via - # caldav - # icalendar - # recurring-ical-events -python-ldap==3.4.5 - # via django-auth-ldap -pytz==2026.2 - # via -r requirements.in -pyyaml==6.0.3 - # via caldav -qh3==1.8.1 - # via urllib3-future -recurring-ical-events==3.8.2 - # via - # caldav - # icalendar-searcher -requests==2.33.1 - # via - # cryptojwt - # simple-openid-connect -sentry-sdk==2.59.0 - # via -r requirements-production.in -simple-openid-connect[django]==2.4.0 - # via -r requirements-production.in -six==1.17.0 - # via - # furl - # orderedmultidict - # python-dateutil -sqlparse==0.5.5 - # via django -tinycss2==1.4.0 - # via bleach -typing-extensions==4.15.0 - # via - # pydantic - # pydantic-core - # typing-inspection -typing-inspection==0.4.2 - # via pydantic -tzdata==2026.2 - # via - # icalendar - # recurring-ical-events - # x-wr-timezone -urllib3==2.7.0 - # via - # requests - # sentry-sdk -urllib3-future==2.20.901 - # via niquests -uwsgi==2.0.31 - # via -r requirements-production.in -wassima==2.1.0 - # via niquests -webencodings==0.5.1 - # via - # bleach - # tinycss2 -x-wr-timezone==2.0.1 - # via recurring-ical-events diff --git a/requirements.in b/requirements.in deleted file mode 100644 index b32e6671..00000000 --- a/requirements.in +++ /dev/null @@ -1,12 +0,0 @@ -django -django-widget-tweaks -django-markdownify -pytz -django-simple-csp -libsasscompiler -django-pipeline -django-friendly-tag-loader -django-encrypted-model-fields -caldav -icalendar -django-token-bucket>=0.2.2 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5621a004..00000000 --- a/requirements.txt +++ /dev/null @@ -1,107 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.14 -# by the following command: -# -# pip-compile --output-file=requirements.txt requirements.in -# -asgiref==3.11.1 - # via django -bleach[css]==6.4.0 - # via django-markdownify -caldav==3.2.0 - # via -r requirements.in -cffi==2.0.0 - # via cryptography -charset-normalizer==3.4.7 - # via niquests -click==8.3.3 - # via x-wr-timezone -cryptography==48.0.1 - # via django-encrypted-model-fields -django==6.0.5 - # via - # -r requirements.in - # django-encrypted-model-fields - # django-markdownify - # django-simple-csp - # django-token-bucket - # django-widget-tweaks -django-encrypted-model-fields==0.6.5 - # via -r requirements.in -django-friendly-tag-loader==1.3.2 - # via -r requirements.in -django-markdownify==0.9.6 - # via -r requirements.in -django-pipeline==3.1.0 - # via - # -r requirements.in - # libsasscompiler -django-simple-csp==0.5.dev1 - # via -r requirements.in -django-token-bucket==0.2.4 - # via -r requirements.in -django-widget-tweaks==1.5.1 - # via -r requirements.in -dnspython==2.8.0 - # via caldav -h11==0.16.0 - # via urllib3-future -icalendar==7.1.0 - # via - # -r requirements.in - # caldav - # icalendar-searcher - # recurring-ical-events - # x-wr-timezone -icalendar-searcher==1.0.5 - # via caldav -jh2==5.0.11 - # via urllib3-future -libsass==0.23.0 - # via libsasscompiler -libsasscompiler==0.2.1 - # via -r requirements.in -lxml==6.1.0 - # via caldav -markdown==3.10.2 - # via django-markdownify -niquests==3.18.8 - # via caldav -pycparser==3.0 - # via cffi -python-dateutil==2.9.0.post0 - # via - # caldav - # icalendar - # recurring-ical-events -pytz==2026.2 - # via -r requirements.in -pyyaml==6.0.3 - # via caldav -qh3==1.8.1 - # via urllib3-future -recurring-ical-events==3.8.2 - # via - # caldav - # icalendar-searcher -six==1.17.0 - # via python-dateutil -sqlparse==0.5.5 - # via django -tinycss2==1.4.0 - # via bleach -tzdata==2026.2 - # via - # icalendar - # recurring-ical-events - # x-wr-timezone -urllib3-future==2.20.901 - # via niquests -wassima==2.1.0 - # via niquests -webencodings==0.5.1 - # via - # bleach - # tinycss2 -x-wr-timezone==2.0.1 - # via recurring-ical-events diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..d7663cfa --- /dev/null +++ b/uv.lock @@ -0,0 +1,1177 @@ +version = 1 +revision = 3 +requires-python = ">=3.12, <3.14" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "asgiref" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, +] + +[[package]] +name = "bitpoll" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "caldav" }, + { name = "django" }, + { name = "django-encrypted-model-fields" }, + { name = "django-friendly-tag-loader" }, + { name = "django-markdownify" }, + { name = "django-pipeline" }, + { name = "django-simple-csp" }, + { name = "django-token-bucket" }, + { name = "django-widget-tweaks" }, + { name = "icalendar" }, + { name = "libsasscompiler" }, + { name = "pytz" }, + { name = "urllib3" }, +] + +[package.optional-dependencies] +dev = [ + { name = "faker" }, + { name = "model-bakery" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, +] +production = [ + { name = "django-auth-ldap" }, + { name = "psycopg2-binary" }, + { name = "sentry-sdk" }, + { name = "simple-openid-connect", extra = ["django"] }, + { name = "uwsgi" }, +] + +[package.dev-dependencies] +dev = [ + { name = "faker" }, + { name = "model-bakery" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, +] + +[package.metadata] +requires-dist = [ + { name = "caldav" }, + { name = "django" }, + { name = "django-auth-ldap", marker = "extra == 'production'" }, + { name = "django-encrypted-model-fields" }, + { name = "django-friendly-tag-loader" }, + { name = "django-markdownify" }, + { name = "django-pipeline" }, + { name = "django-simple-csp" }, + { name = "django-token-bucket", specifier = ">=0.2.2" }, + { name = "django-widget-tweaks" }, + { name = "faker", marker = "extra == 'dev'" }, + { name = "icalendar" }, + { name = "libsasscompiler" }, + { name = "model-bakery", marker = "extra == 'dev'" }, + { name = "psycopg2-binary", marker = "extra == 'production'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "pytest-django", marker = "extra == 'dev'" }, + { name = "pytz" }, + { name = "sentry-sdk", marker = "extra == 'production'" }, + { name = "simple-openid-connect", extras = ["django"], marker = "extra == 'production'" }, + { name = "urllib3" }, + { name = "uwsgi", marker = "extra == 'production'" }, +] +provides-extras = ["production", "dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "faker" }, + { name = "model-bakery" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, +] + +[[package]] +name = "bleach" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "caldav" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "icalendar" }, + { name = "icalendar-searcher" }, + { name = "lxml" }, + { name = "niquests" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "recurring-ical-events" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/20/fd2054d0fe08b2641b5e501fdc1b7d7f989960ce7c2e4ec5a0fad4f8a82b/caldav-3.2.1.tar.gz", hash = "sha256:b474900130226f22dbe3e3abed34559faaeed8a29c455af920c2afb0e355e3d3", size = 10681945, upload-time = "2026-05-28T15:22:26.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/39/970a6699f06487088d8a936ae67e7197d98d11dd778b13b3a1261eac7331/caldav-3.2.1-py3-none-any.whl", hash = "sha256:6869e505ad14741a582003c93e49be2689013d19c9b91679c7c385775b673408", size = 215892, upload-time = "2026-05-28T15:22:20.539Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "cryptojwt" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/85/6de1cadaa68324def632982a983f8498a329c2fe29302e9a3d3bec75bdf5/cryptojwt-1.11.0.tar.gz", hash = "sha256:515a345d3fb091606f293834736d9a72cff932a021a1962f8a8bc6dc0358cfb0", size = 153243, upload-time = "2026-03-24T08:48:21.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f3/815c5e1e110e918c2c431cc9ee0abe79ac24c6a1f012215eb266c5627cad/cryptojwt-1.11.0-py3-none-any.whl", hash = "sha256:156e56b341bb3633e3a0234bab29b3076e3490899510e13b0551dcf0418c814a", size = 90812, upload-time = "2026-03-24T08:48:20.21Z" }, +] + +[[package]] +name = "django" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/42/6cb20996733984c1f6661daeda3877990836c76c633c6c8879d39f7120eb/django-6.1.tar.gz", hash = "sha256:86a2aacd59b817e4d6ac2ebfe22356c58f66f7b24e503f71b7c2fead677ee48b", size = 11223034, upload-time = "2026-08-05T19:21:53.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/9c/ce847620134cfab903e75690c498af73b46abbede2912ea89bd76d5c1e76/django-6.1-py3-none-any.whl", hash = "sha256:6c132cd980c9392b06807d4ca52d72530d631dc65a85d9dacede00a780cefbbe", size = 8417399, upload-time = "2026-08-05T19:21:47.285Z" }, +] + +[[package]] +name = "django-auth-ldap" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-ldap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/6d/d3ceb4b49e7153811a4b2d92bbe198a5ef2e2820469add3d6dc129ef2fab/django_auth_ldap-5.3.0.tar.gz", hash = "sha256:743d8107b146240b46f7e97207dc06cb11facc0cd70dce490b7ca09dd5643d19", size = 55272, upload-time = "2025-12-26T15:00:14.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/91/38ba24b9d76925ce166b2eebe1b4ea460063b8ba8cf91d39d97ee3bad517/django_auth_ldap-5.3.0-py3-none-any.whl", hash = "sha256:aa880415983149b072f876d976ef8ec755a438090e176817998263a6ed9e1038", size = 20975, upload-time = "2025-12-26T15:00:12.52Z" }, +] + +[[package]] +name = "django-encrypted-model-fields" +version = "0.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/44/e4273d48f406b23604cf946912421583a659044d5e521799d1cfd74222c5/django-encrypted-model-fields-0.6.5.tar.gz", hash = "sha256:8bd21c5565c0d64ec4dbd375ad34fea68b4da2db871dec3fa71fe7b5e4221c99", size = 7259, upload-time = "2022-02-24T22:52:12.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/36/1b86079a489f181e33bd83600127e7b3b73c66cd609ac672e58de93ed9a9/django_encrypted_model_fields-0.6.5-py3-none-any.whl", hash = "sha256:b21bbdd8ae2e1a0ea37a5049b3ba46e6e63bf287ad241219a058fac1070796cc", size = 8934, upload-time = "2022-02-24T22:52:13.955Z" }, +] + +[[package]] +name = "django-friendly-tag-loader" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/ee/118e1de58f26fe4826b44fcd56ac9f58f0f8a15864c165fe96ef6a4f8ee5/django_friendly_tag_loader-1.3.2.tar.gz", hash = "sha256:36de040ba8c408b0e6d1fa57ff89b4af1aa9806aaf1cfbaa673861737fb40f78", size = 10447, upload-time = "2024-05-23T02:37:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d7/4749854026d24d29334697e6bf9610af4d5a1aca5dfea649c84f15ad57b0/django_friendly_tag_loader-1.3.2-py3-none-any.whl", hash = "sha256:7c84ad4703b7d989388ee128da5cbe901dbc39eb14174a5a08a888be38392aa5", size = 5378, upload-time = "2024-05-23T02:37:43.08Z" }, +] + +[[package]] +name = "django-markdownify" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleach", extra = ["css"] }, + { name = "django" }, + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/ae/2ecbac028f470ed78b8826413495d5ea60c9d53d7af000729ff3a7663ff8/django_markdownify-0.9.7.tar.gz", hash = "sha256:2e59dfd445ad6362288f7bc1574e45d8356848463e261b878fd70df6a272c47f", size = 8084, upload-time = "2026-05-18T06:51:55.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/b9/9b1a28f605ad75fa7e4588f51661eb3c7c5feb5ad23e985761e9a14d01c4/django_markdownify-0.9.7-py3-none-any.whl", hash = "sha256:ca3a6361f952d1c1b661c13229f69219a36e745358aa0f377212aef8f09e4cf3", size = 10449, upload-time = "2026-05-18T06:51:53.414Z" }, +] + +[[package]] +name = "django-pipeline" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/7b/2f78ccdad9f45a3d4f709950160f9d7e5009b2b8bec8ef636025ec89b62e/django_pipeline-4.1.0.tar.gz", hash = "sha256:aa1d79df6f215b78396cdd50ed162f8741dc4993e9fba2c78483d9b6f1e722b4", size = 72180, upload-time = "2025-09-13T11:47:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/89/fd40adbf5cc16550007ad67bffd7493c9976f7e576bf431d1bc537cfa976/django_pipeline-4.1.0-py3-none-any.whl", hash = "sha256:bdb84feb8db73b9fe8298fd9d0f6e50f30d78eb28a8ed28f73ca5d154080c3d5", size = 75523, upload-time = "2025-09-13T11:42:55.754Z" }, +] + +[[package]] +name = "django-simple-csp" +version = "0.5.dev1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f4/343e43f52f2114ee0c73f4d525f3a3a08b7e13eb97475ad9804cc8c4fdb8/django_simple_csp-0.5.dev1.tar.gz", hash = "sha256:ea302e882303189a61736c83260b79594c8d8cf7a9c765d12afef6320df9aaed", size = 4081, upload-time = "2025-07-08T21:48:11.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/ba/56e9d0258f26bbf96f877ca0f3afcdc6e4399d3e21083dbeda0be8a449d3/django_simple_csp-0.5.dev1-py3-none-any.whl", hash = "sha256:015e0c058d4d72a71e6e0e6c3b1cbf4385795b52e096bce04b16856288c8438a", size = 5054, upload-time = "2025-07-08T21:48:09.79Z" }, +] + +[[package]] +name = "django-token-bucket" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/38/b940b6660f89efa969cce6ef5a43ea96cfa92ba57baf97a11c8dda87e413/django_token_bucket-0.2.4.tar.gz", hash = "sha256:8648f8568d8e5c8d0847f4fafeacede593cf2c2149367356c849bd8647abbc24", size = 6262, upload-time = "2022-05-06T21:39:32.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/38/47c84a291c32213f1348e20cf9e1a3aa1e71b373a0618097b9a4b2cce4ab/django_token_bucket-0.2.4-py3-none-any.whl", hash = "sha256:0e51fc913cae3fa67a2ed441e6e2095c043edc8348b1987ba4c39cdf35c8bc26", size = 7825, upload-time = "2022-05-06T21:39:26.105Z" }, +] + +[[package]] +name = "django-widget-tweaks" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/6d/d1b5a3ae3bccfee96e10315373298cea51e5e0d6853d022181b7b0861a4d/django_widget_tweaks-1.5.1.tar.gz", hash = "sha256:084acc9eeb5a3208f2670522de6284287973e54d54488ce6d402f4b99bc5f452", size = 16233, upload-time = "2026-01-02T12:46:28.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6a/ad176284371005426b9a1c424e6cd77a9018ab1b17dc23948bfbeb2f6a21/django_widget_tweaks-1.5.1-py3-none-any.whl", hash = "sha256:3f5080f8365740fc1c14607498c975cbfed896dd0c40e1b563095716ee31e3b5", size = 9634, upload-time = "2026-01-02T12:46:02.18Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "faker" +version = "40.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/d2/026af1e002bbc6df534d1f8262b18ec79a974f928e9290bfbfdfe7c7b2af/faker-40.36.0.tar.gz", hash = "sha256:754048c76c03afa7de83eee8f4bcee3cf668cbb7d995f54a4e9678db7f110308", size = 2025903, upload-time = "2026-07-24T21:11:33.088Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9a/b947ed175ce9a0dcb070ccf3607f0ce8720cfb5ed1a36166a150b2acd5af/faker-40.36.0-py3-none-any.whl", hash = "sha256:82b9497d9cfe017048075bcf969298a74b1b6e39f5e4dad1211085d1133f7b62", size = 2062829, upload-time = "2026-07-24T21:11:31.37Z" }, +] + +[[package]] +name = "furl" +version = "2.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderedmultidict" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526, upload-time = "2025-03-09T05:36:21.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550, upload-time = "2025-03-09T05:36:19.928Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "icalendar" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/37/4c43e9534a4d43f8cde8f0fd7f869628b45f63f27c5b040230c8f1d90b3d/icalendar-7.2.2.tar.gz", hash = "sha256:6cf904a928881e20c89b682e75bca2eb97e8c5ca629782ed44a519abf4cac1cc", size = 491618, upload-time = "2026-07-20T12:04:05.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/fd/8cbebc368afc020c979e5f9433348531215c65df93b0d6d6f1a1b13a39eb/icalendar-7.2.2-py3-none-any.whl", hash = "sha256:90dd280cb4f67d1ef07b5178c2c33db4fc0a05627e50a3758b6b2aa9b117bc07", size = 502831, upload-time = "2026-07-20T12:04:03.796Z" }, +] + +[[package]] +name = "icalendar-searcher" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "icalendar" }, + { name = "recurring-ical-events" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/65/255f6eb7589c3c98d1d739076c961d60ce70bbb55f7f406589f0245ddc6a/icalendar_searcher-1.0.6.tar.gz", hash = "sha256:2440d4d0d9fbe0021a6647916853744dbca179abd39d25c742ebb8442bbc1bc3", size = 71418, upload-time = "2026-05-29T07:53:29.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/34/2248bb70ca2b4affa2e80623d3bae38a51b435254c3ce17dcff883a44a00/icalendar_searcher-1.0.6-py3-none-any.whl", hash = "sha256:649aad558df211bc50ca3dea3646e1b2f3e93f0995bca2b6358e9c79031471b6", size = 38251, upload-time = "2026-05-29T07:53:28.659Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jh2" +version = "5.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b1/b2b7389b2e0ddac90a1aecbf4a761db8790de85dace7695c01173ed083cc/jh2-5.0.13.tar.gz", hash = "sha256:f8c78cffb3a35c4410513c3eb7989de36028c84277c04f07c97909dd94c23a75", size = 7322505, upload-time = "2026-05-29T05:21:43.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/96/c38d3555cc9bbb1c309c896e99dbb0b1f76308bed7f2d6226cdbeaeb3eee/jh2-5.0.13-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2195557f5953ceee743d0eaab0b09ec537bbc1b9a2c6c1463106bfca9b03805f", size = 601454, upload-time = "2026-05-29T05:19:23.763Z" }, + { url = "https://files.pythonhosted.org/packages/4e/84/a3043ddaf636cec0f4c3aa179a9ef3a59db3d06f770ab6007133ca31bc5f/jh2-5.0.13-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e734f8a5cf002316cc81b80d8a91a0f7c62d631270abd913746644fb9ae288d", size = 384187, upload-time = "2026-05-29T05:19:25.504Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8b/72dba3fb64bf01355a0bbd78cbe0a2635759d4a6f273c1b118760ca8ecf0/jh2-5.0.13-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5e545b3209e8aa5b6af88e7032814473e57d60d32d3fc6b6185453733a4b26", size = 389346, upload-time = "2026-05-29T05:19:26.926Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2e/6f07e42de6101d4555f14be1e4f42a056c9a44d7ee8f745b3c188c96cbe0/jh2-5.0.13-cp313-cp313t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2a37164eb8f2462268e6252db93b009cb685fa060b62ad217bf1b333b450c68d", size = 510278, upload-time = "2026-05-29T05:19:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a5/85682dcc959682aede0f80206a16574c51af51ed26eb1e87252fd31c6ded/jh2-5.0.13-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4eae2d5bc1a91033450fd4381c04f33db7d4c9cfd31046c4708c8c1323c06d0", size = 503090, upload-time = "2026-05-29T05:19:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/a8a301d494c0a1c2859951d44b4fa54f3b2db91826c719ce5be8b662c5c8/jh2-5.0.13-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d0a10036cdf8bf1a36b07c10b69ad0d08ce31d99b8962e3d055ee417fe3423", size = 402843, upload-time = "2026-05-29T05:19:30.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/5f/5f9e0933f458a89005af3b2aa7d1c61cc3cc57c8a633fd54a95acff85121/jh2-5.0.13-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad4d68340e38a8474a48c424e2929d3e2e82e35e4e8ff1188decb1985d1b4d7", size = 386327, upload-time = "2026-05-29T05:19:31.988Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/353ec4a6763e8b6be8d162e6b23f7e1499e75b1bb934cee1e5b5fc062f1e/jh2-5.0.13-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f8c5e81254b986f8abe9fd974e7ed595aeaead3f3066ce9ed95dfff236ceca9", size = 406978, upload-time = "2026-05-29T05:19:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e5/cca58b58cb8bab1891f45da43ef91b498ff93d573a1a2836af11962ccfb8/jh2-5.0.13-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:da030729eb8ef386569e78c10e2429adb10026ce82989fdeb906b037314d66c7", size = 560030, upload-time = "2026-05-29T05:19:34.879Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c9/c3391d8607781016daa1881d8d80f81e0cf0302ff06c86342c492279cf48/jh2-5.0.13-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:a5d2ceb2e9d42ac236f0da7b2f9e8237818d32a10d892b06f30f7160ee4365ae", size = 664407, upload-time = "2026-05-29T05:19:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b4/3375744e2f33d097da241dfd5262c9997a87c2b948db2385fbf100c7dd12/jh2-5.0.13-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:1f4dffe1c6ed0d4b0d2d9a45d064622bb786cbf87db35eac469dd4c376ac6fb6", size = 625242, upload-time = "2026-05-29T05:19:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/e33763b1f66817bd8c848777f05cf3781ad9ee1c642741458bfb66da1921/jh2-5.0.13-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:bdeb48657db508e8bc299744aa431798efdf090feb3959cf0123167c9251ebb0", size = 591124, upload-time = "2026-05-29T05:19:39.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/92/5a8de2f6936c7af77555299732795f4607d2cefb974ad485acf25b3f998b/jh2-5.0.13-cp313-cp313t-win32.whl", hash = "sha256:b41867d5decd7cd62e12cdacfc45c5d8f6eae872578ae01d78900174d5e47b82", size = 238101, upload-time = "2026-05-29T05:19:40.917Z" }, + { url = "https://files.pythonhosted.org/packages/e7/71/0f7ba74bb6681e60e871284772a216a50cfd63f4f62ef52edd13d8a7c057/jh2-5.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:62aee4bd128e17871980e4847cc4b94b67651de612bc96dd3d7ebf647c68a6e4", size = 245923, upload-time = "2026-05-29T05:19:42.338Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dc/11434a6deee70630f5f38cc40813692e1faadd4259f242f4f810f35709eb/jh2-5.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:267aaa7a96f54452e1e07c7701799bc7817e1f3bb2de09301b2c492841e4c98e", size = 242596, upload-time = "2026-05-29T05:19:43.853Z" }, + { url = "https://files.pythonhosted.org/packages/75/f3/8c151f371f7962902e6f1f494b50abc1b8be27305a9d12cb88cf049c5a80/jh2-5.0.13-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3f54095cde010ff4e52f23dda92b31d3d3160c64f5f23fc6e181f8c7938afd33", size = 618581, upload-time = "2026-05-29T05:20:05.043Z" }, + { url = "https://files.pythonhosted.org/packages/71/3f/a2d5458586c024e0076ae8182dcbb71450bd9d4dd65ffdaa659313461bd4/jh2-5.0.13-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f810dd02b4a9647eacbe4eab617111c0e3ffc68e923e3ff172948db601d2ba1e", size = 392776, upload-time = "2026-05-29T05:20:06.178Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1f/cf205104cfcef4bd26aa2b736f36ddf3739b69ddaf14d23a5f7697673d74/jh2-5.0.13-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29c23f3937f257beb5cd5b17f8727404f1267ff25d26e54c0a35d5defd0c895e", size = 397943, upload-time = "2026-05-29T05:20:07.479Z" }, + { url = "https://files.pythonhosted.org/packages/cf/82/bd218132e5a081293cf3d7de5cf41d871f3f88808c23f6c6aebbe073fb5e/jh2-5.0.13-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c697b250a935030a128b45b41f507a4d33a082d8b8e12311c7e73fbb3970f057", size = 520351, upload-time = "2026-05-29T05:20:08.936Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/78b564899237909ddf4103569be59ea31b6c00f8b4f1c178cc57c6e4e45d/jh2-5.0.13-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47d3203ed6fec39df4922764817979fad3a13d22cf750adc9cf9dc2b3399eacf", size = 511162, upload-time = "2026-05-29T05:20:10.239Z" }, + { url = "https://files.pythonhosted.org/packages/db/3a/b59157363139436cb60620353aa8b687197c9a4ec1eb87952004a8843e6e/jh2-5.0.13-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5146468a76bbe8f4035f23a78813c5c433c70c0ca44eb0ba5558627a9e644b8", size = 411838, upload-time = "2026-05-29T05:20:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/15/a4/18b35000b00d9fb72003e0eacf66b3af828a0bb897c21a287a33d2025138/jh2-5.0.13-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d3d338284fbfbe173243c3cc62e510c6fa0d6d023e67da2292bd7578c734e3", size = 395975, upload-time = "2026-05-29T05:20:13.157Z" }, + { url = "https://files.pythonhosted.org/packages/ab/42/f017b6b7b666e6ef615f7c64fd748201d0b20edeed094182c4f37f23a6e8/jh2-5.0.13-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:641f65f5414b8990251bd7609513a696831558f69d2ca77d65dc3f7087f31267", size = 417493, upload-time = "2026-05-29T05:20:14.394Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fc/054443bd40a9827ade99508482185692530cf56a50d4ad8ed2aba761f11d/jh2-5.0.13-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cf3dc6f7c14340241260bf63e5d6006b2ee7db45dafcb39c16fdf164ea0240a0", size = 568373, upload-time = "2026-05-29T05:20:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/47/48/dde093626bb976722c940053ceaa82407bd358e07fa6a1690590456d21b5/jh2-5.0.13-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:fb8e37ecf37382c02af83da521df5a77561441bc4cbb93b00627e015b7bbf349", size = 673766, upload-time = "2026-05-29T05:20:17.136Z" }, + { url = "https://files.pythonhosted.org/packages/96/94/edae999301bb28738e17cf54ecd50b7d7d9fd96d9536376bbeb9b8498526/jh2-5.0.13-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:296ee44dfad08ccc3857daf433dfa4a555d5b45f2df39503ccdd90ff10b32943", size = 635045, upload-time = "2026-05-29T05:20:18.633Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/d9fd0fc71e43c37c30d581affbb7371fa8177ce211b663457939bbed9e61/jh2-5.0.13-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1a75eed545420a31ea356e8046b8d86fcf694234dd1035191734a03720fb8a44", size = 599870, upload-time = "2026-05-29T05:20:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ec/66c17d8aec77d41c8f93145dd7faa7138b9f1bc370fa685008bf92d75f9f/jh2-5.0.13-cp37-abi3-win32.whl", hash = "sha256:4812710810010c428db04e9290a13117f7b7c5a3a663b46a0586fe101cf25c9d", size = 245231, upload-time = "2026-05-29T05:20:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/107f16a84c8becef5281040fdb50681f06e4615bb4a754f211abe7d61a6f/jh2-5.0.13-cp37-abi3-win_amd64.whl", hash = "sha256:5d61bc0a62b4eee11039bf29d4bdbb26efcff760dfb9d8e0739740028aa52b0f", size = 252085, upload-time = "2026-05-29T05:20:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/e8baeef7655449a6c9abee0165fcb1c63d12ec5432b8d810ff5910184690/jh2-5.0.13-cp37-abi3-win_arm64.whl", hash = "sha256:3f74dad9036e599eed51576492b00955237b86cf886f96844708a81cd0f5c710", size = 249139, upload-time = "2026-05-29T05:20:23.558Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/be44f6e9ff5689fcdf9a2c0ed30880fd5da303676d89d6457a603450b7b1/jh2-5.0.13-py3-none-any.whl", hash = "sha256:2b311414989da48a155721fe47d0dc3e3b75360efd62d779aaff72d786be91cf", size = 98909, upload-time = "2026-05-29T05:21:42.086Z" }, +] + +[[package]] +name = "libsass" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b4/ab091585eaa77299558e3289ca206846aefc123fb320b5656ab2542c20ad/libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880", size = 316068, upload-time = "2024-01-06T18:53:05.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/13/fc1bea1de880ca935137183727c7d4dd921c4128fc08b8ddc3698ba5a8a3/libsass-0.23.0-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc", size = 1086783, upload-time = "2024-01-06T19:02:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/6af938651ff3aec0a0b00742209df1172bc297fa73531f292801693b7315/libsass-0.23.0-cp38-abi3-macosx_14_0_arm64.whl", hash = "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6", size = 982759, upload-time = "2024-01-06T19:02:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5a/eb5b62641df0459a3291fc206cf5bd669c0feed7814dded8edef4ade8512/libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306", size = 9444543, upload-time = "2024-01-06T19:02:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fc/275783f5120970d859ae37d04b6a60c13bdec2aa4294b9dfa8a37b5c2513/libsass-0.23.0-cp38-abi3-win32.whl", hash = "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", size = 775481, upload-time = "2024-01-06T19:02:46.05Z" }, + { url = "https://files.pythonhosted.org/packages/ef/20/caf3c7cf2432d85263119798c45221ddf67bdd7dae8f626d14ff8db04040/libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", size = 872914, upload-time = "2024-01-06T19:02:47.61Z" }, +] + +[[package]] +name = "libsasscompiler" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "libsass" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/a5/2b4966be931a86d7ee70577a0faeca1f7652fc67694d86401951e4a3672d/libsasscompiler-0.1.9.tar.gz", hash = "sha256:390f6127ff2d4785383b83a3e94943e3b01cf427251f6d98b3d2a4863d51cc14", size = 3323, upload-time = "2021-11-02T19:09:31.785Z" } + +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "model-bakery" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/49/3e5b51d7af52bf39e348767c1feb16c97d18654a8e3f3306eb4b899e98cf/model_bakery-1.24.0.tar.gz", hash = "sha256:6469223c9fb0eccb021d5c33667b914e9208dd2282d7dc1e55394c2310a7687d", size = 28372, upload-time = "2026-06-23T21:55:23.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/8e/9108507501846c9ee2a1b277d35d1f92ae251852bbd1527f6dbf2665d94a/model_bakery-1.24.0-py3-none-any.whl", hash = "sha256:d1f739cfc43e8d403d04734a18c73c8e677f655dd08719e35342c2f5ada1d7e9", size = 30819, upload-time = "2026-06-23T21:55:21.911Z" }, +] + +[[package]] +name = "niquests" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "urllib3-future" }, + { name = "wassima", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/1c/836ad34fb18fac781722019100e2bffd81eb9a0011d7a91c434a7094460d/niquests-3.21.0.tar.gz", hash = "sha256:5b7d10a05f4c7ed08cede0af74f492ae7a8a5a71291833d029e23365fc3ea80a", size = 1070211, upload-time = "2026-07-29T10:26:24.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/9b/c08403f905f9aaffc74b483fbb0ad4cc372d1da22cccb4ca771a3e65a185/niquests-3.21.0-py3-none-any.whl", hash = "sha256:dbea441b9e9d5d755e02caa2b57b94cdb97b23d754ead21b80a4ac18ef66805f", size = 230382, upload-time = "2026-07-29T10:26:23.194Z" }, +] + +[[package]] +name = "orderedmultidict" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973, upload-time = "2025-11-18T08:00:42.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897, upload-time = "2025-11-18T08:00:41.44Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psycopg2-binary" +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/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { 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" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/ae/5ed1ab2d12090bd794392d798c14d092f0470f6446eaa6d2af4183f4aa2b/pytest_django-4.13.0.tar.gz", hash = "sha256:2316e3f63e3bc799deb4212830e39b2ef1dcc3b2d8688060f1d0217db6a56ebe", size = 94200, upload-time = "2026-08-05T21:36:23.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/88/52db7a840fdbd74934f8df11b7450c9919ac00f61c02e88827f3d58180f7/pytest_django-4.13.0-py3-none-any.whl", hash = "sha256:09374c12d947ca0f99e33726bcb720a6d0124f67058e1130586372bd109493e3", size = 27066, upload-time = "2026-08-05T21:36:21.763Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-ldap" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/60edeb794bbc9ed0ff2149bbaeec605f3ed331766459d195832ecbd0ba2d/python_ldap-3.4.7.tar.gz", hash = "sha256:bacd9fb680d20263d8570ade1cf234d90d281149a8beb4f079dd8f33f7613dc8", size = 387477, upload-time = "2026-05-20T13:41:04.358Z" } + +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "qh3" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/ae/9d42d6df0ab9a014138332346fd690f7b0be0556861421d2459caec28d6f/qh3-1.9.4.tar.gz", hash = "sha256:bd2ea9baf19656c544a48a56a195f2ac257cd973b566f5f2998fa3b7446281a1", size = 346724, upload-time = "2026-07-13T06:36:32.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/b2/7a8f6dcb93a7a23db07c556fe57b167be7b880dd1771d3df62ab1dce4e56/qh3-1.9.4-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:66567176a55bc92f12a7cdaae6d0245f808333a6a55221f37577f79c56848b71", size = 4359072, upload-time = "2026-07-13T06:33:03.308Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ef/343c02a1fe2ee1215120483fee965eab0ee72ea4a9ae122a9b31add14ea6/qh3-1.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a931eba5b6b11292ed65e164a416b78a50139fb9c9dc9dce90006cbd853d1fdd", size = 2159084, upload-time = "2026-07-13T06:33:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/c92e03e4711a1b3e09924d654f3c17dd62b91233302a77ce5695215a2b5b/qh3-1.9.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:325ee1f729e7b2f8600e71e354715db18ec4e669a001449f5924ba0521b9bafe", size = 1862100, upload-time = "2026-07-13T06:33:07.147Z" }, + { url = "https://files.pythonhosted.org/packages/73/9d/88f5088430d25f3a0bc414b15b2b0b22a117468e49bba981c140337bb90b/qh3-1.9.4-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b38c9e5c6552828118676795b0a270e2337571e25d17bf57c18c2c5c346df5", size = 2032620, upload-time = "2026-07-13T06:33:08.738Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/26470349a310715c05776c26e289e54c6a49f08289249445d5a660d364bd/qh3-1.9.4-cp313-cp313t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9cf35ac8f4133a88ad3d8e35d63bfd566f369071a496a16ed69ae09b01b9a64a", size = 2034678, upload-time = "2026-07-13T06:33:10.398Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ae/ad55ebd12852e5c714e1c2956c4497d6d09417e9af5c66eb672b79cdd7f6/qh3-1.9.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e5458cdc9e5214b111d13b86207c2ac799e1191d2e084c442d4fae1ec73fd9b", size = 2032574, upload-time = "2026-07-13T06:33:11.973Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/62b09bdee46d603fd581bec43c98a86c9380f2722d10cb7885d623c1d4e8/qh3-1.9.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486a8dfc1d8eba625e7f92f3a1c45e4a131c7d96d33b73c20d584cf6bb99055", size = 2089542, upload-time = "2026-07-13T06:33:13.591Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/b89476bfd17c47c29425f3b9cdf5bfbe27872853c1d0a53c9315be05c89b/qh3-1.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3e2e90656a46c49ec04e920fbdff28a14eb4439fb660e5de3074c3fbab4ee13", size = 2367301, upload-time = "2026-07-13T06:33:15.142Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/f21458c4ee1b527030f3d23d67ecf97caa75a8a9c0e021ddaac02083413a/qh3-1.9.4-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:4555d49cb3167904a48dec772fb3a71279282c05e70cc2be9d59b597d1fe71a0", size = 2015315, upload-time = "2026-07-13T06:33:17.003Z" }, + { url = "https://files.pythonhosted.org/packages/df/9f/30d83600b6f6bf8a3a995e05b46578e333c8ea5e0a799ea28d5f6f28c262/qh3-1.9.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d5b6a7af8eb57dd168c023ffb041274b35b120a8eddf0a37f9bebcf4d69bf3c9", size = 2349006, upload-time = "2026-07-13T06:33:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/7dc1ac91848f2774c68935da37ecf5e5d069a02b4f98934a74f8868a531a/qh3-1.9.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:593165d62fb259ee9a756e1bf07aa66cb445680e5bbb7fa15d8d3ba59dcf923e", size = 2133242, upload-time = "2026-07-13T06:33:21.12Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/d7d59c1474be53394555b575614eb086d10ed23b7ef3d83f93571ee40fd2/qh3-1.9.4-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:2369dd42c97de4cf628503fd5c02b359b23cabd0ff1773d33b135395748c6e42", size = 2230318, upload-time = "2026-07-13T06:33:22.584Z" }, + { url = "https://files.pythonhosted.org/packages/3e/42/eeca764d3e78b6a3910f95bd33af638f15fc88c6af98807abdc9fb0d75ec/qh3-1.9.4-cp313-cp313t-musllinux_1_1_riscv64.whl", hash = "sha256:eb99f81ad441f64f29da560a0619c9e5b8d9f4532e611c1d6319b536a9d36f53", size = 2125385, upload-time = "2026-07-13T06:33:24.284Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/41d6e4f8d06052873a590557769dfce8a14627e1cc03f74600313ed0c552/qh3-1.9.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2a875175ab7c03d06fdb6784bf207d6aa1d30d2bef8170cde49b6960a0ca96a1", size = 2587129, upload-time = "2026-07-13T06:33:25.841Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/05804bdedc1be747e319091ec770cc8ce5499008b16a70ce2cde035d2558/qh3-1.9.4-cp313-cp313t-win32.whl", hash = "sha256:72c37f9fcf7b1136f245a333bceb7582fb4dcc955bfad0937ca6c857748b5638", size = 1855980, upload-time = "2026-07-13T06:33:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/2c/4eb17c6e4743b8b003ed9c4fffe3acfaa29c000de09a9d8ce6ade1f0b428/qh3-1.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:257a1454732d261b3cf8af90d52cfa13ebb213a899d7983d7707b0c30f4a2533", size = 2122099, upload-time = "2026-07-13T06:33:29.075Z" }, + { url = "https://files.pythonhosted.org/packages/e2/bb/b8101f7a96237516a57b6fbcfebacc2c3349ab0b6ff528784f1c90d69edf/qh3-1.9.4-cp313-cp313t-win_arm64.whl", hash = "sha256:a21c68c0724521fedc4df1abc4402020a1eb37500d138a1bee1c3e50c1b0edb4", size = 1970072, upload-time = "2026-07-13T06:33:30.535Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/646ee80a03ef7fac2b70cccea8a8f17c284fbb86fb854227f6ce70898053/qh3-1.9.4-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:26f7c8ed1a7ac0e25b654f030a4f472830c80890cc284dc40a31c17ecc21fbf0", size = 4376855, upload-time = "2026-07-13T06:34:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/86/d6777ff057a45debcfeb2a55b21692ce0c6f32a3c3a45531e83cf8ed4ef6/qh3-1.9.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57b3dfe5dd2da2084adea63dba0bf587a4096ebf9207339187c90139d1f460d", size = 2164636, upload-time = "2026-07-13T06:34:04.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/7ebfd6babc3aaac74fb40beb5f85c289e2dcc40116282a3e48db99558eb7/qh3-1.9.4-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fa1a3debb5199da3c7f45b0ca6efa55174dfd6229370e774099073143ebe9e9", size = 1865942, upload-time = "2026-07-13T06:34:06.03Z" }, + { url = "https://files.pythonhosted.org/packages/62/8f/bbc0b834b1252356d49d8813156606d426eb7f90ed5bc6aa479bc8aef7c6/qh3-1.9.4-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dc9ff40d3a06f27b47d93417666f03ba6fd6b9585d9711ebbddf84fa4b7c50f", size = 2038977, upload-time = "2026-07-13T06:34:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/c4/32/fe10f124f2062cb9004c18c1b506d29961e2e4d72d2c20ea1b3503ebe6fe/qh3-1.9.4-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:010b132c33719af3df4c1d6f569e52a2a20046c55a31c5532e741019c8c53868", size = 2042505, upload-time = "2026-07-13T06:34:10.932Z" }, + { url = "https://files.pythonhosted.org/packages/c8/eb/b7b93687c033d4291c8207f47b2b0c7fdb738db89ab5beceae08d866734f/qh3-1.9.4-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:124045b0c04c05afc038a239d7717019b4c4b9d0cb967040b4331f4d588eff7d", size = 2039083, upload-time = "2026-07-13T06:34:12.952Z" }, + { url = "https://files.pythonhosted.org/packages/1b/22/b064ec3fd14f121a8710f41bb42680ed96635b55bc78f2e85d475e8dbd65/qh3-1.9.4-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a94a1726bf46a451a0295d68fafde28794e83ab6fda7457cbd9309c38151de9", size = 2092767, upload-time = "2026-07-13T06:34:14.925Z" }, + { url = "https://files.pythonhosted.org/packages/29/92/54bac0ab3aad24b84f1e3c4868399e42cb8678054650f5f2e436acc2554c/qh3-1.9.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b361e627b6a529ecb231c1d5f3f2abd6d1952b0d7cc02c7718d9657d55dae37", size = 2372367, upload-time = "2026-07-13T06:34:16.589Z" }, + { url = "https://files.pythonhosted.org/packages/4c/78/99f27d776e54724050d41a14762e25e6b8d251b20943b59928f4fca366ad/qh3-1.9.4-cp37-abi3-manylinux_2_39_riscv64.whl", hash = "sha256:43b6cfb2240836e3e54bbe61ad1b8fa5716516ff5359131a2421c55676a24f26", size = 2017823, upload-time = "2026-07-13T06:34:18.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f7/ac383f019b69e9c7021e300cde900de36953d44e408fbd323993575ce765/qh3-1.9.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e6db3def610e9c2d2f3602d3e9c8b2156273225f6eedf74a4749d3bc21af8167", size = 2353468, upload-time = "2026-07-13T06:34:19.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/d60b95e872368e51756877e98841b77691acb833e89997839a7adfe053d0/qh3-1.9.4-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:22d5879dc93d2c7ee14cc5933b12c7780d9c7705dff4553becea152fcb767d99", size = 2136024, upload-time = "2026-07-13T06:34:21.837Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/a84cbc776edb96b8502d9e9093432431927dd827ca2d064b196a43418bb7/qh3-1.9.4-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:2c2b2005fe6c605d61cac05ec2b3c019cc546184fa43444f3d1e3d22ed020ab2", size = 2235665, upload-time = "2026-07-13T06:34:23.425Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/ef538579ba7144fadc6865df30bc9ccb3b78ae1dd61899a5da115b0ea461/qh3-1.9.4-cp37-abi3-musllinux_1_1_riscv64.whl", hash = "sha256:e991b0f20b172fea466d0ffd499a85bf2f26decdeeda537275c72b5f7b26ccfe", size = 2128989, upload-time = "2026-07-13T06:34:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ad/fb9f922d0ea410c7f7f19a746f02be0fab2b5b5c061872ca0965fe767c52/qh3-1.9.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c13775ed27a88b297f6a1aa05ffb286879453f99e1e53e0af2b22e7ed8f92ebf", size = 2593826, upload-time = "2026-07-13T06:34:27.288Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/aeef82a0edae7003d42c0587dfbceae4ff0ce375f8c2c13398a4aa4a29f3/qh3-1.9.4-cp37-abi3-win32.whl", hash = "sha256:952abe1be90c0114993886792a8a5ec2211f8391df41227f5b7a5a02ff71bc26", size = 1868838, upload-time = "2026-07-13T06:34:28.944Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/77f3055a3790a515fc59b37cdf1f23c374f8a235c6fa1b110153b78668e4/qh3-1.9.4-cp37-abi3-win_amd64.whl", hash = "sha256:5543cb40a48d401f3fa4ec89398e3a213634f195716a9d06d70417c05edbbb32", size = 2130155, upload-time = "2026-07-13T06:34:30.526Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/aec1f86da0d093a319f43d88b005b4b71efa94e8f675c2c9764098c2abb2/qh3-1.9.4-cp37-abi3-win_arm64.whl", hash = "sha256:d0bd73b3e73f4f80d2430d410b532222ecf57e9d4fe8600b7683c775cbff9479", size = 1980028, upload-time = "2026-07-13T06:34:32.376Z" }, +] + +[[package]] +name = "recurring-ical-events" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "icalendar" }, + { name = "python-dateutil" }, + { name = "tzdata" }, + { name = "x-wr-timezone" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f5/898abd8fbb25766ec6cb17b244730ebf192f47963c71eeacd61aadd903a1/recurring_ical_events-3.8.2.tar.gz", hash = "sha256:e731af31d0b7dec5cd47a1defacd8549e2f36fab1c1995e8b9f042822a0acf8e", size = 604992, upload-time = "2026-04-30T19:21:46.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f7/d3eb4f545baf5d6daaa5881694517626fb7ad04036d94168197a4f021384/recurring_ical_events-3.8.2-py3-none-any.whl", hash = "sha256:9ad605e27b4fbeb70ee1c66205ade32550be15a57cedc579606522f1c67aef6d", size = 241358, upload-time = "2026-04-30T19:21:44.079Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.66.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "simple-openid-connect" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptojwt" }, + { name = "furl" }, + { name = "pydantic" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/03/05babd63b86de8cd5d230b5041b90222a28cf97a33658955465503ce5c18/simple_openid_connect-2.4.0.tar.gz", hash = "sha256:1077319c947f568e08d1d0788eb480e56205564950e4b48951b850f66c073b89", size = 100771, upload-time = "2026-04-04T10:33:26.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/1c/27d722e9883054aec169ae63019613e9a9c44449127a2fa4c8a24a7d1363/simple_openid_connect-2.4.0-py3-none-any.whl", hash = "sha256:03ab140bb885974674df5494165b09d11a43ef99eb3a23ccd5d7909aad5703f2", size = 72238, upload-time = "2026-04-04T10:33:24.011Z" }, +] + +[package.optional-dependencies] +django = [ + { name = "django" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "urllib3-future" +version = "2.24.900" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "jh2" }, + { name = "qh3", marker = "(platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'armv7l' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'i686' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ppc64le' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'riscv64gc' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 's390x' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/9f/590bfa6575e8872829dc65d02c887d0599b3897912479e5ca198660a8c1e/urllib3_future-2.24.900.tar.gz", hash = "sha256:05c2e9d09293ab29a154fed8f5b60644c7cdaffe4ae2587a07b1c7f00f23a7fb", size = 1370419, upload-time = "2026-07-27T06:16:52.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/b6/1a7cf7971c979e56ecff5eb76cf8d748d567ef5f7a4fa39fb87adac922e4/urllib3_future-2.24.900-py3-none-any.whl", hash = "sha256:a4fe06b59b0c3ce2fffa821c3de72eaf59b1e61619becc255dee18919ee25260", size = 809879, upload-time = "2026-07-27T06:16:50.77Z" }, +] + +[[package]] +name = "uwsgi" +version = "2.0.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/49/2f57640e889ba509fd1fae10cccec1b58972a07c2724486efba94c5ea448/uwsgi-2.0.31.tar.gz", hash = "sha256:e8f8b350ccc106ff93a65247b9136f529c14bf96b936ac5b264c6ff9d0c76257", size = 822796, upload-time = "2025-10-11T19:17:28.794Z" } + +[[package]] +name = "wassima" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a1/714674b53d3a57013730187e027e291c652a25db053e02236798bec49d61/wassima-2.1.3.tar.gz", hash = "sha256:fcd6c38be0f909c393da35cb2a993101fcdcff673b8fa8d5da228f73b630d0d0", size = 140075, upload-time = "2026-07-24T18:36:21.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/a2/4c8cd0dddfe40a779739afab2f09e84286bea37cdb73c7b1c12b4990ff12/wassima-2.1.3-py3-none-any.whl", hash = "sha256:f8251c23720dd092117a554b3012756caee3feed6d0122c2458a2aca5d7d92c5", size = 131416, upload-time = "2026-07-24T18:36:19.36Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + +[[package]] +name = "x-wr-timezone" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "icalendar" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/2b/8ae5f59ab852c8fe32dd37c1aa058eb98aca118fec2d3af5c3cd56fffb7b/x_wr_timezone-2.0.1.tar.gz", hash = "sha256:9166c40e6ffd4c0edebabc354e1a1e2cffc1bb473f88007694793757685cc8c3", size = 18212, upload-time = "2025-02-06T17:10:40.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/b7/4bac35b4079b76c07d8faddf89467e9891b1610cfe8d03b0ebb5610e4423/x_wr_timezone-2.0.1-py3-none-any.whl", hash = "sha256:e74a53b9f4f7def8138455c240e65e47c224778bce3c024fcd6da2cbe91ca038", size = 11102, upload-time = "2025-02-06T17:10:39.192Z" }, +]