New Service Request setup (#59)

* [x] Adds a ServiceRequest model for all service types
* [x] Pre-define simple service requests to begin with
* [x] Django admin for admins to handle requests
* [x] Notifications for Matrix
* [x] Moved Service Access to `services` app
* [x] Auto-create default service requests for new memberships
* [x] Most simple kinds of tests added
* [x] Fix issue in generating service requests (check for service access firstly)
* [ ] Channel and bot account

## Deployment

1. Create a bot account. Get an access token with:

   ```
   curl -XPOST \
     -d '{"type":"m.login.password", "user":"<userid>", "password":"<password>"}' \
     "https://data.coop/_matrix/client/r0/login"
   ```

2. Create an admin room for admins. Add admins + bot. Copy the room ID.
3. Add new environment variables for the setup `MATRIX_ACCESS_TOKEN` and `MATRIX_SERVICE_REQUEST_ADMIN_ROOM`

Reviewed-on: https://git.data.coop/data.coop/membersystem/pulls/59
Co-authored-by: bbb <benjamin@overtag.dk>
Co-committed-by: bbb <benjamin@overtag.dk>
This commit is contained in:
bbb 2025-01-15 07:16:12 +00:00 committed by valberg
parent c4df844da6
commit 43d19d3106
29 changed files with 643 additions and 263 deletions

View file

@ -8,3 +8,4 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
DEBUG=True DEBUG=True
STRIPE_API_KEY=sk_test_ STRIPE_API_KEY=sk_test_
STRIPE_ENDPOINT_SECRET=whsec_ STRIPE_ENDPOINT_SECRET=whsec_
MATRIX_ACCESS_TOKEN=

View file

@ -2,7 +2,6 @@ run:
@echo "Running the server" @echo "Running the server"
docker compose up --watch --remove-orphans docker compose up --watch --remove-orphans
[positional-arguments]
manage *ARGS: manage *ARGS:
@echo "Running manage command" @echo "Running manage command"
docker compose run -w /app/src --rm -u `id -u` app python manage.py {{ARGS}} docker compose run -w /app/src --rm -u `id -u` app python manage.py {{ARGS}}
@ -12,7 +11,10 @@ build:
docker compose build docker compose build
typecheck: typecheck:
mypy --config-file=pyproject.toml . docker compose run -w /app/src --rm app mypy .
test:
docker compose run --rm app pytest
# You need to install Stripe CLI from here to run this: https://github.com/stripe/stripe-cli/releases # You need to install Stripe CLI from here to run this: https://github.com/stripe/stripe-cli/releases
stripe_cli: stripe_cli:

View file

@ -6,10 +6,12 @@ requires-python = ">=3.11"
keywords = [] keywords = []
authors = [ authors = [
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" }, { name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
{ name = "Benjamin Balder Bach", email = "benjamin@overtag.dk" },
] ]
dependencies = [ dependencies = [
"Django~=5.1", "Django~=5.1",
"django-allauth~=0.63", "django-allauth~=0.63",
"django-dirtyfields~=1.9.5",
"django-money~=3.5", "django-money~=3.5",
"django-oauth-toolkit~=2.4", "django-oauth-toolkit~=2.4",
"django-registries==0.0.3", "django-registries==0.0.3",
@ -19,6 +21,7 @@ dependencies = [
"django-zen-queries~=2.1", "django-zen-queries~=2.1",
"django_stubs_ext~=5.0", "django_stubs_ext~=5.0",
"environs[django]>=11,<12", "environs[django]>=11,<12",
"httpx~=0.28.1",
"psycopg[binary]~=3.2", "psycopg[binary]~=3.2",
"stripe~=10.5", "stripe~=10.5",
"uvicorn~=0.30", "uvicorn~=0.30",
@ -28,15 +31,14 @@ version = "0.0.1"
[tool.uv] [tool.uv]
dev-dependencies = [ dev-dependencies = [
"coverage[toml]==7.3.0", "coverage[toml]~=7.6",
"pytest==7.2.2", "pytest~=8.3",
"pytest-cov", "pytest-cov",
"pytest-django==4.5.2", "pytest-django~=4.8",
"mypy==1.1.1", "mypy~=1.11",
"django-stubs==1.16.0", "django-stubs[compatible-mypy]~=5.0",
"pip-tools==7.3.0", "django-debug-toolbar~=4.4",
"django-debug-toolbar==4.2.0", "django-browser-reload~=1.15",
"django-browser-reload==1.7.0",
"model-bakery==1.17.0", "model-bakery==1.17.0",
] ]
@ -47,7 +49,7 @@ addopts = "--reuse-db"
norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git" norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git"
python_files = "test*.py" python_files = "test*.py"
testpaths = "tests" testpaths = "tests"
pythonpath = ". tests" pythonpath = ". src tests"
[tool.coverage.run] [tool.coverage.run]
branch = true branch = true
@ -108,6 +110,8 @@ ignore = [
"D105", # Missing docstring in magic method "D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class "D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__` "D107", # Missing docstring in `__init__`
"D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`
"D213", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
"FIX", # TODO, FIXME, XXX "FIX", # TODO, FIXME, XXX
"TD", # TODO, FIXME, XXX "TD", # TODO, FIXME, XXX
"ANN002", # Missing type annotation for `*args` "ANN002", # Missing type annotation for `*args`
@ -127,3 +131,14 @@ force-single-line = true
"D100", # Docstrings "D100", # Docstrings
"D103", # Docstrings "D103", # Docstrings
] ]
"tests/*" = [
"ANN001",
"ANN201",
"ARG001", # TODO: Unused function argument. These are mostly pytest fixtures. Find a way to allow these in tests. Remove this after.
"D103",
"D104",
"S101", # Use of `assert` detected
"PGH004",
"PT004",
"RET504",
]

View file

@ -9,20 +9,26 @@
# - django-stubs-ext~=5.0 # - django-stubs-ext~=5.0
# - django-view-decorator==0.0.4 # - django-view-decorator==0.0.4
# - django-zen-queries~=2.1 # - django-zen-queries~=2.1
# - django<5.2,>=5.1b1 # - django~=5.1
# - environs[django]<12,>=11 # - environs[django]<12,>=11
# - httpx
# - psycopg[binary]~=3.2 # - psycopg[binary]~=3.2
# - stripe~=10.5 # - stripe~=10.5
# - uvicorn~=0.30 # - uvicorn~=0.30
# - whitenoise~=6.7 # - whitenoise~=6.7
# #
anyio==4.7.0
# via httpx
asgiref==3.8.1 asgiref==3.8.1
# via django # via django
babel==2.15.0 babel==2.15.0
# via py-moneyed # via py-moneyed
certifi==2024.7.4 certifi==2024.7.4
# via requests # via
# httpcore
# httpx
# requests
cffi==1.16.0 cffi==1.16.0
# via cryptography # via cryptography
charset-normalizer==3.3.2 charset-normalizer==3.3.2
@ -35,7 +41,7 @@ dj-database-url==2.2.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.1rc1 django==5.1.4
# via # via
# hatch.envs.default # hatch.envs.default
# dj-database-url # dj-database-url
@ -67,9 +73,18 @@ django-zen-queries==2.1.0
environs==11.0.0 environs==11.0.0
# via hatch.envs.default # via hatch.envs.default
h11==0.14.0 h11==0.14.0
# via uvicorn # via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httpx==0.28.1
# via hatch.envs.default
idna==3.7 idna==3.7
# via requests # via
# anyio
# httpx
# requests
jwcrypto==1.5.6 jwcrypto==1.5.6
# via django-oauth-toolkit # via django-oauth-toolkit
marshmallow==3.21.3 marshmallow==3.21.3
@ -96,12 +111,15 @@ requests==2.32.3
# stripe # stripe
setuptools==72.1.0 setuptools==72.1.0
# via django-money # via django-money
sniffio==1.3.1
# via anyio
sqlparse==0.5.1 sqlparse==0.5.1
# via django # via django
stripe==10.6.0 stripe==10.6.0
# via hatch.envs.default # via hatch.envs.default
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
# anyio
# dj-database-url # dj-database-url
# django-stubs-ext # django-stubs-ext
# jwcrypto # jwcrypto

View file

@ -1,15 +1,14 @@
# #
# This file is autogenerated by hatch-pip-compile with Python 3.12 # This file is autogenerated by hatch-pip-compile with Python 3.12
# #
# - coverage[toml]==7.3.0 # - coverage[toml]~=7.6
# - pytest==7.2.2 # - pytest~=8.3
# - pytest-cov # - pytest-cov
# - pytest-django==4.5.2 # - pytest-django~=4.8
# - mypy==1.1.1 # - mypy~=1.11
# - django-stubs==1.16.0 # - django-stubs[compatible-mypy]~=5.0
# - pip-tools==7.3.0 # - django-debug-toolbar~=4.4
# - django-debug-toolbar==4.2.0 # - django-browser-reload~=1.15
# - django-browser-reload==1.7.0
# - model-bakery==1.17.0 # - model-bakery==1.17.0
# - django-allauth~=0.63 # - django-allauth~=0.63
# - django-money~=3.5 # - django-money~=3.5
@ -19,33 +18,36 @@
# - django-stubs-ext~=5.0 # - django-stubs-ext~=5.0
# - django-view-decorator==0.0.4 # - django-view-decorator==0.0.4
# - django-zen-queries~=2.1 # - django-zen-queries~=2.1
# - django<5.2,>=5.1b1 # - django~=5.1
# - environs[django]<12,>=11 # - environs[django]<12,>=11
# - httpx
# - psycopg[binary]~=3.2 # - psycopg[binary]~=3.2
# - stripe~=10.5 # - stripe~=10.5
# - uvicorn~=0.30 # - uvicorn~=0.30
# - whitenoise~=6.7 # - whitenoise~=6.7
# #
anyio==4.7.0
# via httpx
asgiref==3.8.1 asgiref==3.8.1
# via django # via
attrs==23.2.0 # django
# via pytest # django-browser-reload
# django-stubs
babel==2.15.0 babel==2.15.0
# via py-moneyed # via py-moneyed
build==1.2.1
# via pip-tools
certifi==2024.7.4 certifi==2024.7.4
# via requests # via
# httpcore
# httpx
# requests
cffi==1.16.0 cffi==1.16.0
# via cryptography # via cryptography
charset-normalizer==3.3.2 charset-normalizer==3.3.2
# via requests # via requests
click==8.1.7 click==8.1.7
# via # via uvicorn
# pip-tools coverage==7.6.9
# uvicorn
coverage==7.3.0
# via # via
# hatch.envs.dev # hatch.envs.dev
# pytest-cov # pytest-cov
@ -55,7 +57,7 @@ dj-database-url==2.2.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.1rc1 django==5.1.4
# via # via
# hatch.envs.dev # hatch.envs.dev
# dj-database-url # dj-database-url
@ -72,11 +74,11 @@ django==5.1rc1
# model-bakery # model-bakery
django-allauth==0.63.6 django-allauth==0.63.6
# via hatch.envs.dev # via hatch.envs.dev
django-browser-reload==1.7.0 django-browser-reload==1.17.0
# via hatch.envs.dev # via hatch.envs.dev
django-cache-url==3.4.5 django-cache-url==3.4.5
# via environs # via environs
django-debug-toolbar==4.2.0 django-debug-toolbar==4.4.6
# via hatch.envs.dev # via hatch.envs.dev
django-money==3.5.3 django-money==3.5.3
# via hatch.envs.dev # via hatch.envs.dev
@ -86,9 +88,9 @@ django-ratelimit==4.1.0
# via hatch.envs.dev # via hatch.envs.dev
django-registries==0.0.3 django-registries==0.0.3
# via hatch.envs.dev # via hatch.envs.dev
django-stubs==1.16.0 django-stubs==5.1.1
# via hatch.envs.dev # via hatch.envs.dev
django-stubs-ext==5.0.4 django-stubs-ext==5.1.1
# via # via
# hatch.envs.dev # hatch.envs.dev
# django-stubs # django-stubs
@ -99,9 +101,18 @@ django-zen-queries==2.1.0
environs==11.0.0 environs==11.0.0
# via hatch.envs.dev # via hatch.envs.dev
h11==0.14.0 h11==0.14.0
# via uvicorn # via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httpx==0.28.1
# via hatch.envs.dev
idna==3.7 idna==3.7
# via requests # via
# anyio
# httpx
# requests
iniconfig==2.0.0 iniconfig==2.0.0
# via pytest # via pytest
jwcrypto==1.5.6 jwcrypto==1.5.6
@ -110,7 +121,7 @@ marshmallow==3.21.3
# via environs # via environs
model-bakery==1.17.0 model-bakery==1.17.0
# via hatch.envs.dev # via hatch.envs.dev
mypy==1.1.1 mypy==1.13.0
# via # via
# hatch.envs.dev # hatch.envs.dev
# django-stubs # django-stubs
@ -120,13 +131,8 @@ oauthlib==3.2.2
# via django-oauth-toolkit # via django-oauth-toolkit
packaging==24.1 packaging==24.1
# via # via
# build
# marshmallow # marshmallow
# pytest # pytest
pip==24.2
# via pip-tools
pip-tools==7.3.0
# via hatch.envs.dev
pluggy==1.5.0 pluggy==1.5.0
# via pytest # via pytest
psycopg==3.2.1 psycopg==3.2.1
@ -137,16 +143,14 @@ py-moneyed==3.0
# via django-money # via django-money
pycparser==2.22 pycparser==2.22
# via cffi # via cffi
pyproject-hooks==1.1.0 pytest==8.3.4
# via build
pytest==7.2.2
# via # via
# hatch.envs.dev # hatch.envs.dev
# pytest-cov # pytest-cov
# pytest-django # pytest-django
pytest-cov==5.0.0 pytest-cov==5.0.0
# via hatch.envs.dev # via hatch.envs.dev
pytest-django==4.5.2 pytest-django==4.9.0
# via hatch.envs.dev # via hatch.envs.dev
python-dotenv==1.0.1 python-dotenv==1.0.1
# via environs # via environs
@ -157,23 +161,20 @@ requests==2.32.3
# django-oauth-toolkit # django-oauth-toolkit
# stripe # stripe
setuptools==72.1.0 setuptools==72.1.0
# via # via django-money
# django-money sniffio==1.3.1
# pip-tools # via anyio
sqlparse==0.5.1 sqlparse==0.5.1
# via # via
# django # django
# django-debug-toolbar # django-debug-toolbar
stripe==10.6.0 stripe==10.6.0
# via hatch.envs.dev # via hatch.envs.dev
tomli==2.0.1
# via django-stubs
types-pytz==2024.1.0.20240417
# via django-stubs
types-pyyaml==6.0.12.20240724 types-pyyaml==6.0.12.20240724
# via django-stubs # via django-stubs
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
# anyio
# dj-database-url # dj-database-url
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
@ -186,7 +187,5 @@ urllib3==2.2.2
# via requests # via requests
uvicorn==0.30.5 uvicorn==0.30.5
# via hatch.envs.dev # via hatch.envs.dev
wheel==0.43.0
# via pip-tools
whitenoise==6.7.0 whitenoise==6.7.0
# via hatch.envs.dev # via hatch.envs.dev

View file

@ -32,7 +32,7 @@ class Migration(migrations.Migration):
), ),
( (
"created", "created",
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"), models.DateTimeField(auto_now_add=True, verbose_name="created"),
), ),
( (
"owner", "owner",
@ -64,7 +64,7 @@ class Migration(migrations.Migration):
), ),
( (
"created", "created",
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"), models.DateTimeField(auto_now_add=True, verbose_name="created"),
), ),
( (
"description", "description",
@ -139,7 +139,7 @@ class Migration(migrations.Migration):
), ),
( (
"created", "created",
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"), models.DateTimeField(auto_now_add=True, verbose_name="created"),
), ),
( (
"amount_currency", "amount_currency",
@ -194,7 +194,7 @@ class Migration(migrations.Migration):
), ),
( (
"created", "created",
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"), models.DateTimeField(auto_now_add=True, verbose_name="created"),
), ),
( (
"amount_currency", "amount_currency",

View file

@ -17,7 +17,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('name', models.CharField(max_length=1024, verbose_name='description')), ('name', models.CharField(max_length=1024, verbose_name='description')),
('description', models.TextField(blank=True, max_length=2048)), ('description', models.TextField(blank=True, max_length=2048)),
('enabled', models.BooleanField(default=True)), ('enabled', models.BooleanField(default=True)),
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('name', models.CharField(max_length=512)), ('name', models.CharField(max_length=512)),
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), ('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), ('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
@ -63,7 +63,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), ('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), ('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), ('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),

View file

@ -7,20 +7,11 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.db import models from django.db import models
from django.db.models.aggregates import Sum from django.db.models.aggregates import Sum
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy from django.utils.translation import pgettext_lazy
from djmoney.models.fields import MoneyField from djmoney.models.fields import MoneyField
from djmoney.money import Money from djmoney.money import Money
from utils.mixins import CreatedModifiedAbstract
class CreatedModifiedAbstract(models.Model):
"""Abstract model to track creation and modification of objects."""
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
class Meta:
abstract = True
class Account(CreatedModifiedAbstract): class Account(CreatedModifiedAbstract):

View file

@ -20,7 +20,6 @@ from .emails import InviteEmail
from .models import Member from .models import Member
from .models import Membership from .models import Membership
from .models import MembershipType from .models import MembershipType
from .models import ServiceAccess
from .models import SubscriptionPeriod from .models import SubscriptionPeriod
from .models import WaitingListEntry from .models import WaitingListEntry
@ -47,12 +46,6 @@ class SubscriptionPeriodAdmin(admin.ModelAdmin):
"""Admin for SubscriptionPeriod model.""" """Admin for SubscriptionPeriod model."""
@admin.register(ServiceAccess)
class ServiceAccessAdmin(admin.ModelAdmin):
"""Admin for ServiceAccess model."""
pass
class MembershipInlineAdmin(admin.TabularInline): class MembershipInlineAdmin(admin.TabularInline):
"""Inline admin.""" """Inline admin."""

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1rc1 on 2024-12-24 17:32
import django_registries.registry
import services.registry
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('membership', '0011_serviceaccess_alter_membership_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='membership',
options={'verbose_name': 'medlemskab', 'verbose_name_plural': 'medlemskaber'},
),
migrations.AlterField(
model_name='serviceaccess',
name='service',
field=django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service'),
),
]

View file

@ -0,0 +1,16 @@
# Generated by Django 5.1.4 on 2024-12-26 19:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('membership', '0012_alter_membership_options_alter_serviceaccess_service'),
]
operations = [
migrations.DeleteModel(
name='ServiceAccess',
),
]

View file

@ -1,9 +1,9 @@
"""Models for the membership app.""" """Models for the membership app."""
import uuid import uuid
from typing import ClassVar
from typing import Self from typing import Self
from dirtyfields import DirtyFieldsMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.models import UserManager from django.contrib.auth.models import UserManager
from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.constraints import ExclusionConstraint
@ -12,8 +12,8 @@ from django.contrib.postgres.fields import RangeOperators
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from services.registry import ServiceRegistry
from djmoney.money import Money from djmoney.money import Money
from services.models import ServiceRequest
from utils.mixins import CreatedModifiedAbstract from utils.mixins import CreatedModifiedAbstract
@ -105,7 +105,7 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}" return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
class Membership(CreatedModifiedAbstract): class Membership(DirtyFieldsMixin, CreatedModifiedAbstract):
"""A membership. """A membership.
Tracks that a user has membership of a given type for a given period. Tracks that a user has membership of a given type for a given period.
@ -188,6 +188,18 @@ class Membership(CreatedModifiedAbstract):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.user} - {self.period}" return f"{self.user} - {self.period}"
def save(self, *args, **kwargs) -> None:
is_new = not self.pk
# A Membership is considered recently activated when:
# It was created w/ activated=True OR
# It was changed from activated=False to activated=True
# We use django-dirtyfields to detect changes to fields
is_activated = self.activated and (is_new or not self.get_dirty_fields().get("activated", False))
super().save(*args, **kwargs)
# When a membership is activated, we should create service requests
if is_activated:
ServiceRequest.create_defaults(membership=self)
class MembershipType(CreatedModifiedAbstract): class MembershipType(CreatedModifiedAbstract):
"""A membership type. """A membership type.
@ -246,30 +258,3 @@ class WaitingListEntry(CreatedModifiedAbstract):
class Meta: class Meta:
verbose_name = _("waiting list entry") verbose_name = _("waiting list entry")
verbose_name_plural = _("waiting list entries") verbose_name_plural = _("waiting list entries")
class ServiceAccess(CreatedModifiedAbstract):
"""Access to a service for a user."""
class Meta:
verbose_name = _("service access")
verbose_name_plural = _("service accesses")
constraints = (
models.UniqueConstraint(
fields=["user", "service"],
name="unique_user_service",
),
)
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
service = ServiceRegistry.choices_field(verbose_name=_("service"))
subscription_data = models.JSONField(
verbose_name=_("subscription data"),
null=True,
blank=True,
)
def __str__(self) -> str:
return f"{self.user} - {self.service}"

View file

@ -197,6 +197,13 @@ LOGGING = {
STRIPE_API_KEY = env.str("STRIPE_API_KEY", default="") STRIPE_API_KEY = env.str("STRIPE_API_KEY", default="")
STRIPE_ENDPOINT_SECRET = env.str("STRIPE_ENDPOINT_SECRET", default="") STRIPE_ENDPOINT_SECRET = env.str("STRIPE_ENDPOINT_SECRET", default="")
MATRIX_ACCESS_TOKEN = env.str("MATRIX_ACCESS_TOKEN", default="")
MATRIX_SERVICE_REQUEST_ADMIN_ROOM = env.str(
"MATRIX_SERVICE_REQUEST_ADMIN_ROOM",
default="https://matrix.data.coop/_matrix/client/r0/rooms/!wbQCiQKeqangsuUQWm:data.coop/",
)
# The number of seconds a password reset link is valid for (default: 3 days). # The number of seconds a password reset link is valid for (default: 3 days).
# We've extended this to 7 days because invites then last for 1 week. # We've extended this to 7 days because invites then last for 1 week.
PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 7 PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 7

View file

@ -1,13 +1,12 @@
"""Project views.""" """Project views."""
from __future__ import annotations from __future__ import annotations
from membership.models import ServiceAccess
from services.registry import ServiceRegistry
from utils.view_utils import render
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from accounting.models import Order from accounting.models import Order
from django_view_decorator import view from django_view_decorator import view
from utils.view_utils import render
if TYPE_CHECKING: if TYPE_CHECKING:
from django.http import HttpRequest from django.http import HttpRequest

View file

@ -1 +1,15 @@
# Register your models here. from django.contrib import admin
from services.models import ServiceAccess
from services.models import ServiceRequest
@admin.register(ServiceRequest)
class ServiceRequestAdmin(admin.ModelAdmin):
list_display = ("member", "service", "request", "status")
list_filter = ("request", "status")
@admin.register(ServiceAccess)
class ServiceAccessAdmin(admin.ModelAdmin):
"""Admin for ServiceAccess model."""

View file

@ -0,0 +1,36 @@
# Generated by Django 5.1rc1 on 2024-12-24 17:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('membership', '0012_alter_membership_options_alter_serviceaccess_service'),
]
operations = [
migrations.CreateModel(
name='ServiceRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('service', models.CharField(choices=[])),
('request', models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')])),
('is_auto_created', models.BooleanField(default=False)),
('status', models.CharField(choices=[('NEW', 'New'), ('RESOLVED', 'Resolved')], default='NEW', max_length=24)),
('member_notes', models.TextField(blank=True, help_text='Notes from the member, intended to guide the resolution of the request.')),
('admin_notes', models.TextField(blank=True, help_text='Readable by member: Notes from the admin / status updates, resolutions etc.')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.member')),
],
options={
'verbose_name': 'Service Request',
'verbose_name_plural': 'Service Requests',
'constraints': [models.CheckConstraint(condition=models.Q(('status__in', ['NEW', 'RESOLVED'])), name='services_servicerequest_status_valid')],
},
),
]

View file

@ -0,0 +1,52 @@
# Generated by Django 5.1.4 on 2024-12-26 19:45
import django.db.models.deletion
import django_registries.registry
import services.registry
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('membership', '0013_delete_serviceaccess'),
('services', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='servicerequest',
options={'verbose_name': 'service request', 'verbose_name_plural': 'service requests'},
),
migrations.AlterField(
model_name='servicerequest',
name='member',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_requests', to='membership.member'),
),
migrations.AlterField(
model_name='servicerequest',
name='request',
field=models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')], max_length=24),
),
migrations.AlterField(
model_name='servicerequest',
name='service',
field=django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry),
),
migrations.CreateModel(
name='ServiceAccess',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('service', django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service')),
('subscription_data', models.JSONField(blank=True, null=True, verbose_name='subscription data')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member')),
],
options={
'verbose_name': 'service access',
'verbose_name_plural': 'service accesses',
'unique_together': {('member', 'service')},
},
),
]

View file

@ -1 +1,110 @@
# Create your models here. import typing
from django.contrib.sites.models import Site
from django.db import models
from django.db.models import TextChoices
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from utils.matrix import notify_admins
from utils.mixins import CreatedModifiedAbstract
from .registry import ServiceRegistry
from .registry import ServiceRequests
if typing.TYPE_CHECKING:
from membership.models import Membership
class ServiceRequestStatus(TextChoices):
NEW = "NEW", _("New")
RESOLVED = "RESOLVED", _("Resolved")
class ServiceAccess(CreatedModifiedAbstract):
"""Access to a service for a user."""
member = models.ForeignKey("membership.Member", on_delete=models.PROTECT)
service = ServiceRegistry.choices_field(verbose_name=_("service"))
subscription_data = models.JSONField(
verbose_name=_("subscription data"),
null=True,
blank=True,
)
class Meta:
verbose_name = _("service access")
verbose_name_plural = _("service accesses")
constraints = (
models.UniqueConstraint(
fields=["member", "service"],
name="unique_user_service",
),
)
def __str__(self) -> str:
return f"{self.member} - {self.service}"
def save(self, *args, **kwargs) -> None:
"""Ensure that existing ServiceRequest objects are automatically resolved."""
is_new = not self.pk
super().save(*args, **kwargs)
# When a new Service Access is created for a user, we should ensure that all Service Requests for this service
# are set as RESOLVED.
if is_new:
self.member.service_requests.filter(
service=self.service, request=ServiceRequests.CREATION, status=ServiceRequestStatus.NEW
).update(status=ServiceRequestStatus.RESOLVED)
class ServiceRequest(CreatedModifiedAbstract):
member = models.ForeignKey("membership.Member", on_delete=models.CASCADE, related_name="service_requests")
service = ServiceRegistry.choices_field()
request = models.CharField(max_length=24, choices=ServiceRequests.choices)
is_auto_created = models.BooleanField(default=False)
status = models.CharField(max_length=24, choices=ServiceRequestStatus.choices, default=ServiceRequestStatus.NEW)
member_notes = models.TextField(
blank=True, help_text=_("Notes from the member, intended to guide the resolution of the request.")
)
admin_notes = models.TextField(
blank=True, help_text=_("Readable by member: Notes from the admin / status updates, resolutions etc.")
)
class Meta:
verbose_name = _("service request")
verbose_name_plural = _("service requests")
constraints = (
models.CheckConstraint(
name="%(app_label)s_%(class)s_status_valid",
condition=models.Q(status__in=ServiceRequestStatus.values),
),
)
@classmethod
def create_defaults(cls, membership: "Membership") -> None:
"""Ensure that a membership has service requests for all 'default' (auto_create=True) services."""
services_with_access = [
sa.service for sa in ServiceAccess.objects.filter(member=membership.user).values("service")
]
for __, service in ServiceRegistry.get_items():
if service.auto_create and service not in services_with_access:
# Use get_or_create so we don't end up with multiple requests for the same service+user
cls.objects.get_or_create(
member=membership.user, service=service.slug, request=ServiceRequests.CREATION
)
def save(self, *args, **kwargs) -> None:
"""Create notifications when new service requests are added."""
is_new = not self.pk
super().save(*args, **kwargs)
# When a new Service Request is saved, we should send a notification to admins
if is_new:
base_domain = Site.objects.get_current()
change_url = reverse("admin:services_servicerequest_change", kwargs={"object_id": self.pk})
notify_admins(f"New service request: https://{base_domain}{change_url}")

View file

@ -1,6 +1,8 @@
"""Registry for services.""" """Registry for services."""
from django import forms from django import forms
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _
from django_registries.registry import Interface from django_registries.registry import Interface
from django_registries.registry import Registry from django_registries.registry import Registry
@ -11,6 +13,15 @@ class ServiceRegistry(Registry):
implementations_module = "services" implementations_module = "services"
class ServiceRequests(TextChoices):
CREATION = "CREATION", _("Creation")
PASSWORD_RESET = "PASSWORD_RESET", _("Password reset")
DELETION = "DELETION", _("Deletion")
DEFAULT_SERVICE_REQUEST_TYPES = [ServiceRequests.CREATION, ServiceRequests.PASSWORD_RESET, ServiceRequests.DELETION]
class ServiceInterface(Interface): class ServiceInterface(Interface):
"""Interface for services.""" """Interface for services."""
@ -22,6 +33,12 @@ class ServiceInterface(Interface):
public: bool = False public: bool = False
# An auto-created service is added for all new members.
# This is a way of saying that the service is "mandatory" to have.
auto_create: bool = False
request_types: list[str, str] = DEFAULT_SERVICE_REQUEST_TYPES
# TODO: add a way to add a something which defines the required fields for a service # TODO: add a way to add a something which defines the required fields for a service
# - maybe a list of tuples with the field name and the type of the field # - maybe a list of tuples with the field name and the type of the field
# this could be used to generate a form for the service, and also to validate # this could be used to generate a form for the service, and also to validate
@ -36,3 +53,6 @@ class ServiceInterface(Interface):
(forms.Form,), (forms.Form,),
dict(self.subscribe_fields), dict(self.subscribe_fields),
) )
def __str__(self) -> str:
return self.name

View file

@ -26,6 +26,8 @@ class MatrixService(ServiceInterface):
subscribe_fields = (("username", forms.CharField()),) subscribe_fields = (("username", forms.CharField()),)
auto_create = True
class MastodonService(ServiceInterface): class MastodonService(ServiceInterface):
"""Mastodon service.""" """Mastodon service."""
@ -64,6 +66,7 @@ class ForgejoService(ServiceInterface):
name = "Forgejo" name = "Forgejo"
url = "https://git.data.coop" url = "https://git.data.coop"
description = "Git service for data.coop" description = "Git service for data.coop"
auto_create = True
class RalllyService(ServiceInterface): class RalllyService(ServiceInterface):

View file

@ -1 +0,0 @@
# Create your tests here.

View file

@ -6,9 +6,9 @@ from typing import TYPE_CHECKING
from django.shortcuts import redirect from django.shortcuts import redirect
from django_view_decorator import namespaced_decorator_factory from django_view_decorator import namespaced_decorator_factory
from membership.models import ServiceAccess
from utils.view_utils import render from utils.view_utils import render
from services.models import ServiceAccess
from services.registry import ServiceInterface from services.registry import ServiceInterface
from services.registry import ServiceRegistry from services.registry import ServiceRegistry

25
src/utils/matrix.py Normal file
View file

@ -0,0 +1,25 @@
from urllib.parse import urljoin
import httpx
from django.conf import settings
def notify_admins(message: str) -> None:
"""Notify admins on their own Matrix channel."""
return notify_matrix_channel(settings.MATRIX_SERVICE_REQUEST_ADMIN_ROOM, message)
def notify_matrix_channel(channel_url: str, message: str) -> None:
"""Send a message to a matrix channel."""
result = httpx.post(
urljoin(channel_url, "send/m.room.message"),
json={
"msgtype": "m.text",
"body": message,
},
params={
"access_token": settings.MATRIX_ACCESS_TOKEN,
},
)
if settings.MATRIX_ACCESS_TOKEN and not settings.DEBUG:
result.raise_for_status()

0
tests/__init__.py Normal file
View file

41
tests/conftest.py Normal file
View file

@ -0,0 +1,41 @@
from datetime import timedelta
from unittest import mock
import pytest
from django.db.backends.postgresql.psycopg_any import DateRange
from django.utils import timezone
from membership.models import Member
from membership.models import Membership
from membership.models import MembershipType
from membership.models import SubscriptionPeriod
@pytest.fixture()
def membership_type():
return MembershipType.objects.create(name="Test Membership Type")
@pytest.fixture()
def current_period():
SubscriptionPeriod.objects.create(
period=DateRange(timezone.now().date() - timedelta(days=182), timezone.now().date() + timedelta(days=183))
)
return SubscriptionPeriod.objects.current()
@pytest.fixture()
def active_membership(membership_type, current_period):
member = Member.objects.create_user("test", "lala@adas.com", "1234")
membership = Membership.objects.create(
user=member,
membership_type=membership_type,
period=current_period,
activated=True,
)
return membership
@pytest.fixture(autouse=True)
def mock_matrix_notify():
with mock.patch("utils.matrix.httpx.post"):
yield

1
tests/settings.py Normal file
View file

@ -0,0 +1 @@
from project.settings import * # noqa

View file

@ -1,12 +1,7 @@
import pytest import pytest
from accounting import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from . import models
# @pytest.fixture
# def test():
# do stuff
@pytest.mark.django_db() @pytest.mark.django_db()
def test_balance() -> None: def test_balance() -> None:

12
tests/test_services.py Normal file
View file

@ -0,0 +1,12 @@
import pytest
from membership.models import Membership
from services.models import ServiceRequest
from services.models import ServiceRequestStatus
@pytest.mark.django_db()
def test_membership_activation(active_membership: Membership):
assert ServiceRequest.objects.filter(
member=active_membership.user,
status=ServiceRequestStatus.NEW,
).exists()

291
uv.lock generated
View file

@ -1,6 +1,20 @@
version = 1 version = 1
requires-python = ">=3.11" requires-python = ">=3.11"
[[package]]
name = "anyio"
version = "4.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 },
]
[[package]] [[package]]
name = "asgiref" name = "asgiref"
version = "3.8.1" version = "3.8.1"
@ -10,15 +24,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
] ]
[[package]]
name = "attrs"
version = "24.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
]
[[package]] [[package]]
name = "babel" name = "babel"
version = "2.16.0" version = "2.16.0"
@ -28,20 +33,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 },
] ]
[[package]]
name = "build"
version = "1.2.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "os_name == 'nt'" },
{ name = "packaging" },
{ name = "pyproject-hooks" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950 },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.12.14" version = "2024.12.14"
@ -167,30 +158,50 @@ wheels = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.3.0" version = "7.6.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/87/c0163d39ac70cab62ebcaee164c988215cd312919a78940c2251a2fcfabb/coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865", size = 763902 } sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/c5/c94da7b5ee14a0e7b046b2d59b50fe37d50ae78046e3459639961d3dccf5/coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f", size = 201209 }, { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 },
{ url = "https://files.pythonhosted.org/packages/56/61/0bc551ef5e4cd459c34e769969b080d667ea9b2b3265819d4ae1f8d07702/coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d", size = 201423 }, { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 },
{ url = "https://files.pythonhosted.org/packages/7a/6b/f16c757f34adaf76413b061ff412d599958a299dba5dfb9371e5567b77d9/coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd", size = 233474 }, { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 },
{ url = "https://files.pythonhosted.org/packages/62/b9/de6fc3a608b4c0438b96e120fe83304d39b6be640b14363004843602118d/coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7", size = 231048 }, { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 },
{ url = "https://files.pythonhosted.org/packages/55/63/f2dcc8f7f1587ae54bf8cc1c3b08e07e442633a953537dfaf658a0cbac2c/coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a", size = 232856 }, { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 },
{ url = "https://files.pythonhosted.org/packages/44/39/809e546b31d871e9636315d0097891ae3177e0f6da2021c489f64dbe00b7/coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74", size = 241805 }, { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 },
{ url = "https://files.pythonhosted.org/packages/2a/b2/f2b519d33ececf73cf3d616fc7d051a73aa9609859fde376e902d79b69ce/coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214", size = 240219 }, { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 },
{ url = "https://files.pythonhosted.org/packages/c5/ad/1559ab85952a47531004f9a32bcac51f9755e9541fb03eae42a9358e00dd/coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f", size = 241271 }, { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 },
{ url = "https://files.pythonhosted.org/packages/32/5a/d8e474e01fde6511bf8354df005248aeb2e3a71dacfe1624fbc2916a15f4/coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", size = 203467 }, { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 },
{ url = "https://files.pythonhosted.org/packages/b1/cb/48d62b864e408bea2608b4ce19ba1feba0ffbf5a03640cf024cb3122e895/coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70", size = 204490 }, { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 },
{ url = "https://files.pythonhosted.org/packages/dd/53/2de98835e2976d042fd30967e6b00d57e688cfcc17ad10f11dc2c307ec9c/coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b", size = 201331 }, { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 },
{ url = "https://files.pythonhosted.org/packages/9b/01/49a4f47d87acc3be6cd0013c33b7ef6e1acc13f67ac9ff2fd1f7d73b4b12/coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446", size = 201429 }, { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 },
{ url = "https://files.pythonhosted.org/packages/05/1d/45d448cfa9cdf7aea9ec49711a143c82afc793e9542f9ba9e3f5b83c4d4d/coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071", size = 234294 }, { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 },
{ url = "https://files.pythonhosted.org/packages/a9/43/29bb5ceabd87bdff07ac29333a68828f210e7c2e928c85464e9264f7a8df/coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe", size = 231652 }, { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 },
{ url = "https://files.pythonhosted.org/packages/82/a6/194198e62702d82ee581a035fcc5032a7bebc0264eb5ebffb466c6b5b4ea/coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a", size = 233627 }, { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 },
{ url = "https://files.pythonhosted.org/packages/5b/5b/4e7ec6cc17a0cb4afc1aa99e6877d5e2c6377cdfeac67dba39643e1d4809/coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873", size = 240463 }, { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 },
{ url = "https://files.pythonhosted.org/packages/d1/6b/b7f5e6e7ae64f0b8795dfb499ba73a5bae66131b518c1e5c448fb838d3c9/coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2", size = 238427 }, { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 },
{ url = "https://files.pythonhosted.org/packages/01/40/a0f76d77a9a64947fc3dac90b0f62fbd7f4d02e62d10a7126f6785eb2cbe/coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b", size = 240139 }, { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 },
{ url = "https://files.pythonhosted.org/packages/e3/b9/6244d38d1574bd13995025802dbc5577acd5aab143e53ddecc087d485a30/coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", size = 203768 }, { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 },
{ url = "https://files.pythonhosted.org/packages/17/11/48d4804db0f3b0277a857b57ade93f03cb9f2afbce0e07c208a9f9b01805/coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479", size = 204653 }, { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 },
{ url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 },
{ url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 },
{ url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 },
{ url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 },
{ url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 },
{ url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 },
{ url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 },
{ url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 },
{ url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 },
{ url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 },
{ url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 },
{ url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 },
{ url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 },
{ url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 },
{ url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 },
{ url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 },
{ url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 },
{ url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 },
{ url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 },
{ url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -276,14 +287,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/a1/6f/3ccc016b901d7e5a1
[[package]] [[package]]
name = "django-browser-reload" name = "django-browser-reload"
version = "1.7.0" version = "1.17.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" },
{ name = "django" }, { name = "django" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/73/1f/aceee2f52d4c1d68289faac23ee7b5ad4597bd637aa17f41c10f9c59fe55/django_browser_reload-1.7.0.tar.gz", hash = "sha256:712d0a4d6caa6833c8c205d4ce177b474feb45583cd6ee684c9ea7b71dd921b6", size = 15924 } sdist = { url = "https://files.pythonhosted.org/packages/9f/bc/3c67f7daca53b826ec51888576fe5e117d9442d2d0acb58f4264d48b9dba/django_browser_reload-1.17.0.tar.gz", hash = "sha256:3667939cde0eee1a6d698dbe3b78cf10b573dabc4e711fb7933f1ba91fb98da4", size = 14312 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/56/12a04d5d34acd8dd7e01b6268a1d341220d78280b63115a5ef6470233103/django_browser_reload-1.7.0-py3-none-any.whl", hash = "sha256:0d7cc4308ebbdf9b5637e28ee7b41ae28d4fcdf350d55ce007515b44cd10291b", size = 12070 }, { url = "https://files.pythonhosted.org/packages/7f/8f/62fc4fbf5c05c2210e6cb616f1c2a3da53871dfecbaa4c44b1f482ca3e8f/django_browser_reload-1.17.0-py3-none-any.whl", hash = "sha256:d372c12c1c5962c02279a53cac7e8a020c48f104592c637a06d0768b28d2d6be", size = 12228 },
] ]
[[package]] [[package]]
@ -297,15 +309,27 @@ wheels = [
[[package]] [[package]]
name = "django-debug-toolbar" name = "django-debug-toolbar"
version = "4.2.0" version = "4.4.6"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "sqlparse" }, { name = "sqlparse" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/65/bd/81b812b3a69874f382514a8982f1e7c30e9851e86c9d976eef4960da97ac/django_debug_toolbar-4.2.0.tar.gz", hash = "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc", size = 259709 } sdist = { url = "https://files.pythonhosted.org/packages/d4/9c/0a3238eda0a46df20f2e3fe2a30313d34f5042a1a737d08230b77c29a3e9/django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044", size = 272610 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/5c/fffae0d49d0b9d6a0782540e172272edf70493588ec9fca10b01a3a75c3e/django_debug_toolbar-4.2.0-py3-none-any.whl", hash = "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327", size = 223156 }, { url = "https://files.pythonhosted.org/packages/2f/33/2036a472eedfbe49240dffea965242b3f444de4ea4fbeceb82ccea33a2ce/django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45", size = 229621 },
]
[[package]]
name = "django-dirtyfields"
version = "1.9.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/c6/5f9a642ad623f8a66e538aadfa002a99561e794b20539575ac50c72bfda3/django_dirtyfields-1.9.5.tar.gz", hash = "sha256:c316ab2e12cfc9da16e714007f0b313d35c3216f6b90b7b1d66c50bf1f4f3f19", size = 21664 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/50/33/98b2d301e9167ca5bca286a2a153d0640a49e96fdb584e77cecf726afbd6/django_dirtyfields-1.9.5-py3-none-any.whl", hash = "sha256:d544e648df2f13256683c3e20b93f61672b63087acf3f81ee8cedb4b0482a8c1", size = 8445 },
] ]
[[package]] [[package]]
@ -361,20 +385,23 @@ wheels = [
[[package]] [[package]]
name = "django-stubs" name = "django-stubs"
version = "1.16.0" version = "5.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" },
{ name = "django" }, { name = "django" },
{ name = "django-stubs-ext" }, { name = "django-stubs-ext" },
{ name = "mypy" },
{ name = "tomli" },
{ name = "types-pytz" },
{ name = "types-pyyaml" }, { name = "types-pyyaml" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/0e2323bf6fd080623976006860421d47ca4a6d5d21e27980010c187a05ad/django-stubs-1.16.0.tar.gz", hash = "sha256:1bd96207576cd220221a0e615f0259f13d453d515a80f576c1246e0fb547f561", size = 236630 } sdist = { url = "https://files.pythonhosted.org/packages/bf/60/1ae90eb6e2e107bc64a3de9de78a5add7f3b85e491113504eed38d6d2c63/django_stubs-5.1.1.tar.gz", hash = "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", size = 265624 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/3e/9c1a097b80002e340d5b3f4b6777a79501a1a5cba29a18f137b9fb91aa91/django_stubs-1.16.0-py3-none-any.whl", hash = "sha256:c95f948e2bfc565f3147e969ff361ef033841a0b8a51cac974a6cc6d0486732c", size = 432684 }, { url = "https://files.pythonhosted.org/packages/98/c8/3081d5f994351248fcd60f9aab10cb2020bdd7df0f14e80854373e15d7d4/django_stubs-5.1.1-py3-none-any.whl", hash = "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac", size = 470790 },
]
[package.optional-dependencies]
compatible-mypy = [
{ name = "mypy" },
] ]
[[package]] [[package]]
@ -443,6 +470,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
] ]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@ -493,6 +548,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-allauth" }, { name = "django-allauth" },
{ name = "django-dirtyfields" },
{ name = "django-money" }, { name = "django-money" },
{ name = "django-oauth-toolkit" }, { name = "django-oauth-toolkit" },
{ name = "django-ratelimit" }, { name = "django-ratelimit" },
@ -501,6 +557,7 @@ dependencies = [
{ name = "django-view-decorator" }, { name = "django-view-decorator" },
{ name = "django-zen-queries" }, { name = "django-zen-queries" },
{ name = "environs", extra = ["django"] }, { name = "environs", extra = ["django"] },
{ name = "httpx" },
{ name = "psycopg", extra = ["binary"] }, { name = "psycopg", extra = ["binary"] },
{ name = "stripe" }, { name = "stripe" },
{ name = "uvicorn" }, { name = "uvicorn" },
@ -512,10 +569,9 @@ dev = [
{ name = "coverage", extra = ["toml"] }, { name = "coverage", extra = ["toml"] },
{ name = "django-browser-reload" }, { name = "django-browser-reload" },
{ name = "django-debug-toolbar" }, { name = "django-debug-toolbar" },
{ name = "django-stubs" }, { name = "django-stubs", extra = ["compatible-mypy"] },
{ name = "model-bakery" }, { name = "model-bakery" },
{ name = "mypy" }, { name = "mypy" },
{ name = "pip-tools" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-django" }, { name = "pytest-django" },
@ -525,6 +581,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "django", specifier = "~=5.1" }, { name = "django", specifier = "~=5.1" },
{ name = "django-allauth", specifier = "~=0.63" }, { name = "django-allauth", specifier = "~=0.63" },
{ name = "django-dirtyfields", specifier = "~=1.9.5" },
{ name = "django-money", specifier = "~=3.5" }, { name = "django-money", specifier = "~=3.5" },
{ name = "django-oauth-toolkit", specifier = "~=2.4" }, { name = "django-oauth-toolkit", specifier = "~=2.4" },
{ name = "django-ratelimit", specifier = "~=4.1" }, { name = "django-ratelimit", specifier = "~=4.1" },
@ -533,6 +590,7 @@ requires-dist = [
{ name = "django-view-decorator", specifier = "==0.0.4" }, { name = "django-view-decorator", specifier = "==0.0.4" },
{ name = "django-zen-queries", specifier = "~=2.1" }, { name = "django-zen-queries", specifier = "~=2.1" },
{ name = "environs", extras = ["django"], specifier = ">=11,<12" }, { name = "environs", extras = ["django"], specifier = ">=11,<12" },
{ name = "httpx", specifier = "~=0.28.1" },
{ name = "psycopg", extras = ["binary"], specifier = "~=3.2" }, { name = "psycopg", extras = ["binary"], specifier = "~=3.2" },
{ name = "stripe", specifier = "~=10.5" }, { name = "stripe", specifier = "~=10.5" },
{ name = "uvicorn", specifier = "~=0.30" }, { name = "uvicorn", specifier = "~=0.30" },
@ -541,16 +599,15 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "coverage", extras = ["toml"], specifier = "==7.3.0" }, { name = "coverage", extras = ["toml"], specifier = "~=7.6" },
{ name = "django-browser-reload", specifier = "==1.7.0" }, { name = "django-browser-reload", specifier = "~=1.15" },
{ name = "django-debug-toolbar", specifier = "==4.2.0" }, { name = "django-debug-toolbar", specifier = "~=4.4" },
{ name = "django-stubs", specifier = "==1.16.0" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = "~=5.0" },
{ name = "model-bakery", specifier = "==1.17.0" }, { name = "model-bakery", specifier = "==1.17.0" },
{ name = "mypy", specifier = "==1.1.1" }, { name = "mypy", specifier = "~=1.11" },
{ name = "pip-tools", specifier = "==7.3.0" }, { name = "pytest", specifier = "~=8.3" },
{ name = "pytest", specifier = "==7.2.2" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-django", specifier = "==4.5.2" }, { name = "pytest-django", specifier = "~=4.8" },
] ]
[[package]] [[package]]
@ -567,20 +624,30 @@ wheels = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.1.1" version = "1.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "mypy-extensions" }, { name = "mypy-extensions" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/62/54/be80f8d01f5cf72f774a77f9f750527a6fa733f09f78b1da30e8fa3914e6/mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f", size = 2778293 } sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/ab/d6d3884c3f432898458e2ade712988a7d1da562c1a363f2003b31677acd8/mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389", size = 10475489 }, { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
{ url = "https://files.pythonhosted.org/packages/b9/e5/71eef5239219ee2f4d85e2ca6368d736705a3b874023b57f7237b977839c/mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2", size = 9567148 }, { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
{ url = "https://files.pythonhosted.org/packages/bf/2d/45a526f248719ee32ecf1261564247a2e717a9c6167de5eb67d53599c4df/mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5", size = 11978458 }, { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
{ url = "https://files.pythonhosted.org/packages/64/63/6a04ca7a8b7f34811cada43ed6119736a7f4a07c5e1cbd8eec0e0f4962d5/mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c", size = 12055007 }, { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
{ url = "https://files.pythonhosted.org/packages/ed/89/85a04f32135fe4e35fd59d47100c939c7425fcb29868894c4b7a6171e065/mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54", size = 8862912 }, { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
{ url = "https://files.pythonhosted.org/packages/a4/0b/3a30f50287e42a4230320fa2eac25eb3017d38a7c31f083d407ab627607c/mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4", size = 2373884 }, { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
{ url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
{ url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
{ url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
{ url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
{ url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
{ url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
] ]
[[package]] [[package]]
@ -610,31 +677,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
] ]
[[package]]
name = "pip"
version = "24.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 },
]
[[package]]
name = "pip-tools"
version = "7.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "build" },
{ name = "click" },
{ name = "pip" },
{ name = "setuptools" },
{ name = "wheel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/01/f0055058a86a888f32ac794fa68d5a25c2d2f7a3e8181474b711faaa2145/pip-tools-7.3.0.tar.gz", hash = "sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d", size = 136178 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/df/47e6267c6b5cdae867adbdd84b437393e6202ce4322de0a5e0b92960e1d6/pip_tools-7.3.0-py3-none-any.whl", hash = "sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e", size = 57367 },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.5.0" version = "1.5.0"
@ -724,29 +766,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
] ]
[[package]]
name = "pyproject-hooks"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.2.2" version = "8.3.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "attrs" },
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" }, { name = "iniconfig" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pluggy" }, { name = "pluggy" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b9/29/311895d9cd3f003dd58e8fdea36dd895ba2da5c0c90601836f7de79f76fe/pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4", size = 1320028 } sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/68/5321b5793bd506961bd40bdbdd0674e7de4fb873ee7cab33dd27283ad513/pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", size = 317207 }, { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
] ]
[[package]] [[package]]
@ -764,14 +796,14 @@ wheels = [
[[package]] [[package]]
name = "pytest-django" name = "pytest-django"
version = "4.5.2" version = "4.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pytest" }, { name = "pytest" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9b/42/6d6563165b82289d4a30ea477f85c04386303e51cf4e4e4651d4f9910830/pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2", size = 79949 } sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/21/b65ecd6686da400e2f6e3c49c2a428325abd979c9670cd97e1671f53296e/pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e", size = 20752 }, { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 },
] ]
[[package]] [[package]]
@ -816,6 +848,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.3" version = "0.5.3"
@ -877,15 +918,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
] ]
[[package]]
name = "types-pytz"
version = "2024.2.0.20241221"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/26/516311b02b5a215e721155fb65db8a965d061372e388d6125ebce8d674b0/types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9", size = 10213 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/db/c92ca6920cccd9c2998b013601542e2ac5e59bc805bcff94c94ad254b7df/types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5", size = 10008 },
]
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.20241221" version = "6.0.12.20241221"
@ -935,15 +967,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
] ]
[[package]]
name = "wheel"
version = "0.45.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 },
]
[[package]] [[package]]
name = "whitenoise" name = "whitenoise"
version = "6.8.2" version = "6.8.2"