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:
parent
c4df844da6
commit
43d19d3106
29 changed files with 643 additions and 263 deletions
|
@ -8,3 +8,4 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
|||
DEBUG=True
|
||||
STRIPE_API_KEY=sk_test_
|
||||
STRIPE_ENDPOINT_SECRET=whsec_
|
||||
MATRIX_ACCESS_TOKEN=
|
||||
|
|
6
Justfile
6
Justfile
|
@ -2,7 +2,6 @@ run:
|
|||
@echo "Running the server"
|
||||
docker compose up --watch --remove-orphans
|
||||
|
||||
[positional-arguments]
|
||||
manage *ARGS:
|
||||
@echo "Running manage command"
|
||||
docker compose run -w /app/src --rm -u `id -u` app python manage.py {{ARGS}}
|
||||
|
@ -12,7 +11,10 @@ build:
|
|||
docker compose build
|
||||
|
||||
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
|
||||
stripe_cli:
|
||||
|
|
|
@ -6,10 +6,12 @@ requires-python = ">=3.11"
|
|||
keywords = []
|
||||
authors = [
|
||||
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
|
||||
{ name = "Benjamin Balder Bach", email = "benjamin@overtag.dk" },
|
||||
]
|
||||
dependencies = [
|
||||
"Django~=5.1",
|
||||
"django-allauth~=0.63",
|
||||
"django-dirtyfields~=1.9.5",
|
||||
"django-money~=3.5",
|
||||
"django-oauth-toolkit~=2.4",
|
||||
"django-registries==0.0.3",
|
||||
|
@ -19,6 +21,7 @@ dependencies = [
|
|||
"django-zen-queries~=2.1",
|
||||
"django_stubs_ext~=5.0",
|
||||
"environs[django]>=11,<12",
|
||||
"httpx~=0.28.1",
|
||||
"psycopg[binary]~=3.2",
|
||||
"stripe~=10.5",
|
||||
"uvicorn~=0.30",
|
||||
|
@ -28,15 +31,14 @@ version = "0.0.1"
|
|||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"coverage[toml]==7.3.0",
|
||||
"pytest==7.2.2",
|
||||
"coverage[toml]~=7.6",
|
||||
"pytest~=8.3",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.5.2",
|
||||
"mypy==1.1.1",
|
||||
"django-stubs==1.16.0",
|
||||
"pip-tools==7.3.0",
|
||||
"django-debug-toolbar==4.2.0",
|
||||
"django-browser-reload==1.7.0",
|
||||
"pytest-django~=4.8",
|
||||
"mypy~=1.11",
|
||||
"django-stubs[compatible-mypy]~=5.0",
|
||||
"django-debug-toolbar~=4.4",
|
||||
"django-browser-reload~=1.15",
|
||||
"model-bakery==1.17.0",
|
||||
]
|
||||
|
||||
|
@ -47,7 +49,7 @@ addopts = "--reuse-db"
|
|||
norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git"
|
||||
python_files = "test*.py"
|
||||
testpaths = "tests"
|
||||
pythonpath = ". tests"
|
||||
pythonpath = ". src tests"
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
@ -108,6 +110,8 @@ ignore = [
|
|||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"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
|
||||
"TD", # TODO, FIXME, XXX
|
||||
"ANN002", # Missing type annotation for `*args`
|
||||
|
@ -127,3 +131,14 @@ force-single-line = true
|
|||
"D100", # 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",
|
||||
]
|
||||
|
|
|
@ -9,20 +9,26 @@
|
|||
# - django-stubs-ext~=5.0
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries~=2.1
|
||||
# - django<5.2,>=5.1b1
|
||||
# - django~=5.1
|
||||
# - environs[django]<12,>=11
|
||||
# - httpx
|
||||
# - psycopg[binary]~=3.2
|
||||
# - stripe~=10.5
|
||||
# - uvicorn~=0.30
|
||||
# - whitenoise~=6.7
|
||||
#
|
||||
|
||||
anyio==4.7.0
|
||||
# via httpx
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
babel==2.15.0
|
||||
# via py-moneyed
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
cffi==1.16.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.3.2
|
||||
|
@ -35,7 +41,7 @@ dj-database-url==2.2.0
|
|||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.1rc1
|
||||
django==5.1.4
|
||||
# via
|
||||
# hatch.envs.default
|
||||
# dj-database-url
|
||||
|
@ -67,9 +73,18 @@ django-zen-queries==2.1.0
|
|||
environs==11.0.0
|
||||
# via hatch.envs.default
|
||||
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
|
||||
# via requests
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
jwcrypto==1.5.6
|
||||
# via django-oauth-toolkit
|
||||
marshmallow==3.21.3
|
||||
|
@ -96,12 +111,15 @@ requests==2.32.3
|
|||
# stripe
|
||||
setuptools==72.1.0
|
||||
# via django-money
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
sqlparse==0.5.1
|
||||
# via django
|
||||
stripe==10.6.0
|
||||
# via hatch.envs.default
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# anyio
|
||||
# dj-database-url
|
||||
# django-stubs-ext
|
||||
# jwcrypto
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
#
|
||||
# This file is autogenerated by hatch-pip-compile with Python 3.12
|
||||
#
|
||||
# - coverage[toml]==7.3.0
|
||||
# - pytest==7.2.2
|
||||
# - coverage[toml]~=7.6
|
||||
# - pytest~=8.3
|
||||
# - pytest-cov
|
||||
# - pytest-django==4.5.2
|
||||
# - mypy==1.1.1
|
||||
# - django-stubs==1.16.0
|
||||
# - pip-tools==7.3.0
|
||||
# - django-debug-toolbar==4.2.0
|
||||
# - django-browser-reload==1.7.0
|
||||
# - pytest-django~=4.8
|
||||
# - mypy~=1.11
|
||||
# - django-stubs[compatible-mypy]~=5.0
|
||||
# - django-debug-toolbar~=4.4
|
||||
# - django-browser-reload~=1.15
|
||||
# - model-bakery==1.17.0
|
||||
# - django-allauth~=0.63
|
||||
# - django-money~=3.5
|
||||
|
@ -19,33 +18,36 @@
|
|||
# - django-stubs-ext~=5.0
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries~=2.1
|
||||
# - django<5.2,>=5.1b1
|
||||
# - django~=5.1
|
||||
# - environs[django]<12,>=11
|
||||
# - httpx
|
||||
# - psycopg[binary]~=3.2
|
||||
# - stripe~=10.5
|
||||
# - uvicorn~=0.30
|
||||
# - whitenoise~=6.7
|
||||
#
|
||||
|
||||
anyio==4.7.0
|
||||
# via httpx
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
attrs==23.2.0
|
||||
# via pytest
|
||||
# via
|
||||
# django
|
||||
# django-browser-reload
|
||||
# django-stubs
|
||||
babel==2.15.0
|
||||
# via py-moneyed
|
||||
build==1.2.1
|
||||
# via pip-tools
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
cffi==1.16.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# pip-tools
|
||||
# uvicorn
|
||||
coverage==7.3.0
|
||||
# via uvicorn
|
||||
coverage==7.6.9
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# pytest-cov
|
||||
|
@ -55,7 +57,7 @@ dj-database-url==2.2.0
|
|||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.1rc1
|
||||
django==5.1.4
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# dj-database-url
|
||||
|
@ -72,11 +74,11 @@ django==5.1rc1
|
|||
# model-bakery
|
||||
django-allauth==0.63.6
|
||||
# via hatch.envs.dev
|
||||
django-browser-reload==1.7.0
|
||||
django-browser-reload==1.17.0
|
||||
# via hatch.envs.dev
|
||||
django-cache-url==3.4.5
|
||||
# via environs
|
||||
django-debug-toolbar==4.2.0
|
||||
django-debug-toolbar==4.4.6
|
||||
# via hatch.envs.dev
|
||||
django-money==3.5.3
|
||||
# via hatch.envs.dev
|
||||
|
@ -86,9 +88,9 @@ django-ratelimit==4.1.0
|
|||
# via hatch.envs.dev
|
||||
django-registries==0.0.3
|
||||
# via hatch.envs.dev
|
||||
django-stubs==1.16.0
|
||||
django-stubs==5.1.1
|
||||
# via hatch.envs.dev
|
||||
django-stubs-ext==5.0.4
|
||||
django-stubs-ext==5.1.1
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# django-stubs
|
||||
|
@ -99,9 +101,18 @@ django-zen-queries==2.1.0
|
|||
environs==11.0.0
|
||||
# via hatch.envs.dev
|
||||
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
|
||||
# via requests
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
jwcrypto==1.5.6
|
||||
|
@ -110,7 +121,7 @@ marshmallow==3.21.3
|
|||
# via environs
|
||||
model-bakery==1.17.0
|
||||
# via hatch.envs.dev
|
||||
mypy==1.1.1
|
||||
mypy==1.13.0
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# django-stubs
|
||||
|
@ -120,13 +131,8 @@ oauthlib==3.2.2
|
|||
# via django-oauth-toolkit
|
||||
packaging==24.1
|
||||
# via
|
||||
# build
|
||||
# marshmallow
|
||||
# pytest
|
||||
pip==24.2
|
||||
# via pip-tools
|
||||
pip-tools==7.3.0
|
||||
# via hatch.envs.dev
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
psycopg==3.2.1
|
||||
|
@ -137,16 +143,14 @@ py-moneyed==3.0
|
|||
# via django-money
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pytest==7.2.2
|
||||
pytest==8.3.4
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# pytest-cov
|
||||
# pytest-django
|
||||
pytest-cov==5.0.0
|
||||
# via hatch.envs.dev
|
||||
pytest-django==4.5.2
|
||||
pytest-django==4.9.0
|
||||
# via hatch.envs.dev
|
||||
python-dotenv==1.0.1
|
||||
# via environs
|
||||
|
@ -157,23 +161,20 @@ requests==2.32.3
|
|||
# django-oauth-toolkit
|
||||
# stripe
|
||||
setuptools==72.1.0
|
||||
# via
|
||||
# django-money
|
||||
# pip-tools
|
||||
# via django-money
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
sqlparse==0.5.1
|
||||
# via
|
||||
# django
|
||||
# django-debug-toolbar
|
||||
stripe==10.6.0
|
||||
# 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
|
||||
# via django-stubs
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# anyio
|
||||
# dj-database-url
|
||||
# django-stubs
|
||||
# django-stubs-ext
|
||||
|
@ -186,7 +187,5 @@ urllib3==2.2.2
|
|||
# via requests
|
||||
uvicorn==0.30.5
|
||||
# via hatch.envs.dev
|
||||
wheel==0.43.0
|
||||
# via pip-tools
|
||||
whitenoise==6.7.0
|
||||
# via hatch.envs.dev
|
||||
|
|
|
@ -32,7 +32,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
|
@ -64,7 +64,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
|
@ -139,7 +139,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
(
|
||||
"amount_currency",
|
||||
|
@ -194,7 +194,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
(
|
||||
"amount_currency",
|
||||
|
|
|
@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(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='oprettet')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('name', models.CharField(max_length=1024, verbose_name='description')),
|
||||
('description', models.TextField(blank=True, max_length=2048)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
|
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(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='oprettet')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('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)),
|
||||
|
@ -63,7 +63,7 @@ class Migration(migrations.Migration):
|
|||
fields=[
|
||||
('id', models.AutoField(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='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', 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)),
|
||||
|
|
|
@ -7,20 +7,11 @@ from django.conf import settings
|
|||
from django.contrib import admin
|
||||
from django.db import models
|
||||
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 djmoney.models.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
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
|
||||
from utils.mixins import CreatedModifiedAbstract
|
||||
|
||||
|
||||
class Account(CreatedModifiedAbstract):
|
||||
|
|
|
@ -20,7 +20,6 @@ from .emails import InviteEmail
|
|||
from .models import Member
|
||||
from .models import Membership
|
||||
from .models import MembershipType
|
||||
from .models import ServiceAccess
|
||||
from .models import SubscriptionPeriod
|
||||
from .models import WaitingListEntry
|
||||
|
||||
|
@ -47,12 +46,6 @@ class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
|||
"""Admin for SubscriptionPeriod model."""
|
||||
|
||||
|
||||
@admin.register(ServiceAccess)
|
||||
class ServiceAccessAdmin(admin.ModelAdmin):
|
||||
"""Admin for ServiceAccess model."""
|
||||
pass
|
||||
|
||||
|
||||
class MembershipInlineAdmin(admin.TabularInline):
|
||||
"""Inline admin."""
|
||||
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
16
src/membership/migrations/0013_delete_serviceaccess.py
Normal file
16
src/membership/migrations/0013_delete_serviceaccess.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -1,9 +1,9 @@
|
|||
"""Models for the membership app."""
|
||||
|
||||
import uuid
|
||||
from typing import ClassVar
|
||||
from typing import Self
|
||||
|
||||
from dirtyfields import DirtyFieldsMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import UserManager
|
||||
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.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from services.registry import ServiceRegistry
|
||||
from djmoney.money import Money
|
||||
from services.models import ServiceRequest
|
||||
from utils.mixins import CreatedModifiedAbstract
|
||||
|
||||
|
||||
|
@ -105,7 +105,7 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
|
|||
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
||||
|
||||
|
||||
class Membership(CreatedModifiedAbstract):
|
||||
class Membership(DirtyFieldsMixin, CreatedModifiedAbstract):
|
||||
"""A membership.
|
||||
|
||||
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:
|
||||
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):
|
||||
"""A membership type.
|
||||
|
@ -246,30 +258,3 @@ class WaitingListEntry(CreatedModifiedAbstract):
|
|||
class Meta:
|
||||
verbose_name = _("waiting list entry")
|
||||
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}"
|
||||
|
|
|
@ -197,6 +197,13 @@ LOGGING = {
|
|||
STRIPE_API_KEY = env.str("STRIPE_API_KEY", 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).
|
||||
# We've extended this to 7 days because invites then last for 1 week.
|
||||
PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 7
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
"""Project views."""
|
||||
|
||||
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 accounting.models import Order
|
||||
from django_view_decorator import view
|
||||
from utils.view_utils import render
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import HttpRequest
|
||||
|
|
|
@ -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."""
|
||||
|
|
36
src/services/migrations/0001_initial.py
Normal file
36
src/services/migrations/0001_initial.py
Normal 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')],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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}")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Registry for services."""
|
||||
|
||||
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 Registry
|
||||
|
||||
|
@ -11,6 +13,15 @@ class ServiceRegistry(Registry):
|
|||
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):
|
||||
"""Interface for services."""
|
||||
|
||||
|
@ -22,6 +33,12 @@ class ServiceInterface(Interface):
|
|||
|
||||
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
|
||||
# - 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
|
||||
|
@ -36,3 +53,6 @@ class ServiceInterface(Interface):
|
|||
(forms.Form,),
|
||||
dict(self.subscribe_fields),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
|
|
@ -26,6 +26,8 @@ class MatrixService(ServiceInterface):
|
|||
|
||||
subscribe_fields = (("username", forms.CharField()),)
|
||||
|
||||
auto_create = True
|
||||
|
||||
|
||||
class MastodonService(ServiceInterface):
|
||||
"""Mastodon service."""
|
||||
|
@ -64,6 +66,7 @@ class ForgejoService(ServiceInterface):
|
|||
name = "Forgejo"
|
||||
url = "https://git.data.coop"
|
||||
description = "Git service for data.coop"
|
||||
auto_create = True
|
||||
|
||||
|
||||
class RalllyService(ServiceInterface):
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
# Create your tests here.
|
|
@ -6,9 +6,9 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from django.shortcuts import redirect
|
||||
from django_view_decorator import namespaced_decorator_factory
|
||||
from membership.models import ServiceAccess
|
||||
from utils.view_utils import render
|
||||
|
||||
from services.models import ServiceAccess
|
||||
from services.registry import ServiceInterface
|
||||
from services.registry import ServiceRegistry
|
||||
|
||||
|
|
25
src/utils/matrix.py
Normal file
25
src/utils/matrix.py
Normal 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
0
tests/__init__.py
Normal file
41
tests/conftest.py
Normal file
41
tests/conftest.py
Normal 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
1
tests/settings.py
Normal file
|
@ -0,0 +1 @@
|
|||
from project.settings import * # noqa
|
|
@ -1,12 +1,7 @@
|
|||
import pytest
|
||||
from accounting import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from . import models
|
||||
|
||||
# @pytest.fixture
|
||||
# def test():
|
||||
# do stuff
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_balance() -> None:
|
12
tests/test_services.py
Normal file
12
tests/test_services.py
Normal 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
291
uv.lock
generated
|
@ -1,6 +1,20 @@
|
|||
version = 1
|
||||
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]]
|
||||
name = "asgiref"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "babel"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "certifi"
|
||||
version = "2024.12.14"
|
||||
|
@ -167,30 +158,50 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.3.0"
|
||||
version = "7.6.10"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/32/5a/d8e474e01fde6511bf8354df005248aeb2e3a71dacfe1624fbc2916a15f4/coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482", size = 203467 },
|
||||
{ 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/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/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/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/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/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/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/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/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/e3/b9/6244d38d1574bd13995025802dbc5577acd5aab143e53ddecc087d485a30/coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321", size = 203768 },
|
||||
{ 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/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/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/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/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/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/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/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/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/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 },
|
||||
{ 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/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/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/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/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/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/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/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/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/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 },
|
||||
{ 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]
|
||||
|
@ -276,14 +287,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/a1/6f/3ccc016b901d7e5a1
|
|||
|
||||
[[package]]
|
||||
name = "django-browser-reload"
|
||||
version = "1.7.0"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
@ -297,15 +309,27 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "django-debug-toolbar"
|
||||
version = "4.2.0"
|
||||
version = "4.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
@ -361,20 +385,23 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "django-stubs"
|
||||
version = "1.16.0"
|
||||
version = "5.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
{ name = "django-stubs-ext" },
|
||||
{ name = "mypy" },
|
||||
{ name = "tomli" },
|
||||
{ name = "types-pytz" },
|
||||
{ name = "types-pyyaml" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
@ -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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
|
@ -493,6 +548,7 @@ source = { virtual = "." }
|
|||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-allauth" },
|
||||
{ name = "django-dirtyfields" },
|
||||
{ name = "django-money" },
|
||||
{ name = "django-oauth-toolkit" },
|
||||
{ name = "django-ratelimit" },
|
||||
|
@ -501,6 +557,7 @@ dependencies = [
|
|||
{ name = "django-view-decorator" },
|
||||
{ name = "django-zen-queries" },
|
||||
{ name = "environs", extra = ["django"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "stripe" },
|
||||
{ name = "uvicorn" },
|
||||
|
@ -512,10 +569,9 @@ dev = [
|
|||
{ name = "coverage", extra = ["toml"] },
|
||||
{ name = "django-browser-reload" },
|
||||
{ name = "django-debug-toolbar" },
|
||||
{ name = "django-stubs" },
|
||||
{ name = "django-stubs", extra = ["compatible-mypy"] },
|
||||
{ name = "model-bakery" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pip-tools" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
|
@ -525,6 +581,7 @@ dev = [
|
|||
requires-dist = [
|
||||
{ name = "django", specifier = "~=5.1" },
|
||||
{ name = "django-allauth", specifier = "~=0.63" },
|
||||
{ name = "django-dirtyfields", specifier = "~=1.9.5" },
|
||||
{ name = "django-money", specifier = "~=3.5" },
|
||||
{ name = "django-oauth-toolkit", specifier = "~=2.4" },
|
||||
{ name = "django-ratelimit", specifier = "~=4.1" },
|
||||
|
@ -533,6 +590,7 @@ requires-dist = [
|
|||
{ name = "django-view-decorator", specifier = "==0.0.4" },
|
||||
{ name = "django-zen-queries", specifier = "~=2.1" },
|
||||
{ name = "environs", extras = ["django"], specifier = ">=11,<12" },
|
||||
{ name = "httpx", specifier = "~=0.28.1" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = "~=3.2" },
|
||||
{ name = "stripe", specifier = "~=10.5" },
|
||||
{ name = "uvicorn", specifier = "~=0.30" },
|
||||
|
@ -541,16 +599,15 @@ requires-dist = [
|
|||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "coverage", extras = ["toml"], specifier = "==7.3.0" },
|
||||
{ name = "django-browser-reload", specifier = "==1.7.0" },
|
||||
{ name = "django-debug-toolbar", specifier = "==4.2.0" },
|
||||
{ name = "django-stubs", specifier = "==1.16.0" },
|
||||
{ name = "coverage", extras = ["toml"], specifier = "~=7.6" },
|
||||
{ name = "django-browser-reload", specifier = "~=1.15" },
|
||||
{ name = "django-debug-toolbar", specifier = "~=4.4" },
|
||||
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "~=5.0" },
|
||||
{ name = "model-bakery", specifier = "==1.17.0" },
|
||||
{ name = "mypy", specifier = "==1.1.1" },
|
||||
{ name = "pip-tools", specifier = "==7.3.0" },
|
||||
{ name = "pytest", specifier = "==7.2.2" },
|
||||
{ name = "mypy", specifier = "~=1.11" },
|
||||
{ name = "pytest", specifier = "~=8.3" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django", specifier = "==4.5.2" },
|
||||
{ name = "pytest-django", specifier = "~=4.8" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -567,20 +624,30 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.1.1"
|
||||
version = "1.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mypy-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 = [
|
||||
{ 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/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/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/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/ed/89/85a04f32135fe4e35fd59d47100c939c7425fcb29868894c4b7a6171e065/mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54", size = 8862912 },
|
||||
{ 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/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/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/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/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/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
|
||||
{ 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]]
|
||||
|
@ -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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pluggy"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pytest"
|
||||
version = "7.2.2"
|
||||
version = "8.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
@ -764,14 +796,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pytest-django"
|
||||
version = "4.5.2"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
@ -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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "sqlparse"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "types-pyyaml"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "whitenoise"
|
||||
version = "6.8.2"
|
||||
|
|
Loading…
Add table
Reference in a new issue