diff --git a/.env.example b/.env.example
index 07f88a4..645ad68 100644
--- a/.env.example
+++ b/.env.example
@@ -1,13 +1,27 @@
-# Project environment
+# Project environment: dev | prod
ENVIRONMENT=dev
# Django settings
DJANGO_SECRET_KEY=your_secret_key_here
DJANGO_DEBUG=True
-DJANGO_ALLOWED_HOSTS=localhost,
+DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
+# DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com
+# DJANGO_LOG_LEVEL=INFO
-# Database settings
-DATABASE_URL=postgres://user:password@localhost:5432/dbname
+# Database settings (по умолчанию — sqlite, для PostgreSQL раскомментируйте)
+# DATABASE_URL=postgres://app:app@localhost:5432/app
+
+# Postgres (используется docker-compose)
+POSTGRES_DB=app
+POSTGRES_USER=app
+POSTGRES_PASSWORD=app
# Redis settings
-REDIS_URL=redis://localhost:6379/0
\ No newline at end of file
+REDIS_URL=redis://localhost:6379/1
+REDIS_HOST=localhost
+REDIS_PORT=6379
+
+# Production security (используются при ENVIRONMENT=prod)
+# DJANGO_SECURE_SSL_REDIRECT=True
+# DJANGO_SECURE_HSTS_SECONDS=2592000
+# GUNICORN_WORKERS=4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1f0af00
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,45 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --group dev
+
+ - name: Ruff check
+ run: uv run ruff check .
+
+ - name: Ruff format check
+ run: uv run ruff format --check .
+
+ - name: Bandit
+ run: uv run bandit -c pyproject.toml -r .
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --group dev
+
+ - name: Run tests
+ run: uv run pytest -v
diff --git a/Dockerfile b/Dockerfile
index c086bc1..a5db3bf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
ARG UID=1000
-ARG PYTHON_VERSION=3.13.7
+ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim-trixie AS builder
@@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
+ARG UID
RUN useradd --create-home --shell /bin/bash --uid ${UID:-1000} django
USER django
WORKDIR /home/django/app
diff --git a/Makefile b/Makefile
index df5d283..2658280 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,44 @@
-.PHONY: env
+.PHONY: env install run migrate makemigrations superuser test lint format up down logs
env:
@if [ -f .env ]; then \
echo ".env уже существует, пропускаю."; \
else \
cp .env.example .env && echo "Создано .env из .env.example."; \
- fi
\ No newline at end of file
+ fi
+
+install:
+ uv sync --group dev
+
+run:
+ uv run python manage.py runserver
+
+migrate:
+ uv run python manage.py migrate
+
+makemigrations:
+ uv run python manage.py makemigrations
+
+superuser:
+ uv run python manage.py createsuperuser
+
+test:
+ uv run pytest
+
+lint:
+ uv run ruff check .
+ uv run ruff format --check .
+ uv run bandit -c pyproject.toml -r .
+
+format:
+ uv run ruff check --fix .
+ uv run ruff format .
+
+up:
+ docker compose up -d --build
+
+down:
+ docker compose down
+
+logs:
+ docker compose logs -f django
diff --git a/README.md b/README.md
index b88a643..1d98784 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,214 @@
-### Общая архитектура
-
-Проект построен как **монолитное API-ориентированное приложение** (API-first Monolith). Архитектура спроектирована для быстрого старта разработки, но с запасом прочности для масштабирования до HighLoad-системы. В основе лежит принцип **модульности**: весь бизнес-функционал инкапсулирован в изолированные приложения внутри директории `apps/`.
-
-Система следует стандартам **Twelve-Factor App**, что обеспечивает её готовность к развертыванию в облачных средах (Kubernetes/Docker) и легкость в горизонтальном масштабировании.
-
-### Уровни системы
-
-Архитектура разделена на логические слои, чтобы снизить связность кода (Loose Coupling):
-
-1. **Инфраструктурный слой (`config/`, `pyproject.toml`)**:
- * Централизованная конфигурация, отделенная от кода бизнес-логики.
- * Управление зависимостями через современный стандарт PEP 735 (dependency groups), что упрощает CI/CD пайплайны.
- * Сюда входят настройки безопасности, логирования и подключений к БД.
-2. **Слой API (Interface Layer)**:
- * Отвечает за принятие запросов и валидацию входных данных.
- * Реализован на **Django REST Framework**.
- * Строгое разделение: ViewSet'ы занимаются только HTTP-логикой, не содержат бизнес-правил.
-3. **Слой бизнес-логики (Domain Layer)**:
- * Расположен внутри приложений в `apps/`.
- * Инкапсулирует правила работы системы. Здесь живут сервисы (Services) и селекторы (Selectors), которые отделены от моделей и view.
- * Модели данных ("толстые модели" используются умеренно) отвечают только за целостность данных, но не за сложные процессы.
-4. **Слой данных (Persistence Layer)**:
- * PostgreSQL как основное хранилище.
- * Использование миграций Django для версионирования схемы БД.
-
-### Ключевые архитектурные решения
-
-* **Изоляция приложений**:
-Каждый модуль в папке `apps/` (например, `users`, `billing`) проектируется как независимый компонент с собственным пространством имен (`apps.users`). Это предотвращает "спагетти-импорты" и облегчает будущий вынос модулей в микросервисы при необходимости.
-* **Безопасность по умолчанию**:
-В архитектуру встроены инструменты статического анализа (`bandit`, `ruff`), которые на этапе разработки блокируют уязвимый код. Конфигурация Django (`settings.py`) ужесточена для production-среды.
-* **Строгая типизация и качество кода**:
-Проект не допускает неявных типов. Использование `mypy` и `ruff` гарантирует, что кодовая база остается чистой, читаемой и предсказуемой, что критично для командной разработки.
-* **Готовность к Observability**:
-Архитектура подразумевает, что логирование и метрики (Tracing/Metrics) внедряются через middleware и не загрязняют бизнес-код, что упрощает подключение систем мониторинга (Prometheus/Grafana/Sentry).
-
-
----
-
-Это **Modern Python Backend** архитектура: она отказывается от устаревших практик (как requirements.txt или запутанных settings) в пользу строгих стандартов, типизации и модульности. Она идеально подходит для команд, где важны скорость разработки (Time-to-Market) и надежность (Reliability).
+
+
+
+
+[](https://git.io/typing-svg)
+
+[](https://github.com/ak4code/django_boilerplate/actions/workflows/ci.yml)
+[](https://www.python.org/)
+[](https://www.djangoproject.com/)
+[](https://www.django-rest-framework.org/)
+[](https://www.postgresql.org/)
+[](https://redis.io/)
+[](https://www.docker.com/)
+[](https://docs.astral.sh/uv/)
+[](https://docs.astral.sh/ruff/)
+
+
+
+---
+
+## ✨ Возможности
+
+| | |
+| --- | --- |
+| 🏗️ **API-first монолит** | Слои: views → services / selectors → models |
+| 🔖 **Версионирование через заголовок** | `AcceptHeaderVersioning` — чистые URL без `/v1/` |
+| 📖 **OpenAPI из коробки** | Swagger UI и ReDoc (drf-spectacular) |
+| ⚙️ **12-factor конфигурация** | Всё через переменные окружения (django-environ) |
+| 🔒 **Безопасный production** | HSTS, secure cookies, SSL redirect, whitenoise, gunicorn |
+| 👤 **Кастомный пользователь** | Email-логин + `external_id` (UUID) для интеграций |
+| 💓 **Health-check** | `/health/` с проверкой БД и Redis — готов для k8s-проб |
+| 🧹 **Качество кода** | ruff · bandit · ty · pre-commit · pytest · GitHub Actions |
+| 🐳 **Docker** | Multi-stage образ (non-root) + compose с PostgreSQL и Redis |
+
+## 🚀 Быстрый старт
+
+### 🐳 Docker (рекомендуется)
+
+```bash
+make env # создать .env из .env.example
+make up # собрать и запустить postgres + redis + django
+make logs # смотреть логи
+```
+
+➜ Приложение: http://localhost:8000 · Swagger UI: http://localhost:8000/api/docs/
+
+### 💻 Локально
+
+```bash
+make env
+make install # uv sync --group dev
+make migrate
+make run
+```
+
+> 💡 По умолчанию локально используется SQLite; для PostgreSQL задайте `DATABASE_URL` в `.env`.
+
+## 🔖 Версионирование API
+
+Используется `AcceptHeaderVersioning`: клиент передаёт версию в заголовке `Accept`,
+URL остаются стабильными (`/api/users/...` без `/v1/`).
+
+```bash
+# ✅ Явно указанная версия
+curl http://localhost:8000/api/users/me/ \
+ -H 'Accept: application/json; version=v1'
+
+# ✅ Без версии — используется DEFAULT_VERSION (v1)
+curl http://localhost:8000/api/users/me/
+
+# ❌ Неподдерживаемая версия → 406 Not Acceptable
+curl http://localhost:8000/api/users/me/ \
+ -H 'Accept: application/json; version=v999'
+```
+
+Версия доступна во view через `request.version`. Список поддерживаемых версий —
+`ALLOWED_VERSIONS` в `config/settings/base.py`. Чтобы выпустить `v2`, добавьте версию
+в `ALLOWED_VERSIONS` и ветвите логику по `request.version` (или подключайте другой
+сериализатор), не меняя URL.
+
+## 🌐 Эндпоинты
+
+| Метод | URL | Описание |
+| :---: | --- | --- |
+| `POST` | `/api/users/register/` | 📝 Регистрация пользователя |
+| `GET` | `/api/users/me/` | 👤 Профиль текущего пользователя |
+| `GET` | `/api/schema/` | 📄 OpenAPI-схема |
+| `GET` | `/api/docs/` | 📖 Swagger UI |
+| `GET` | `/api/redoc/` | 📕 ReDoc |
+| `GET` | `/health/` | 💓 Health-check (БД + Redis), 503 при сбое |
+| — | `/admin/` | 🛠️ Django admin |
+
+## 🏛️ Архитектура
+
+Монолитное API-ориентированное приложение по стандартам **Twelve-Factor App**.
+Бизнес-функционал инкапсулирован в изолированные приложения внутри `apps/` —
+каждый модуль проектируется как независимый компонент, что облегчает будущий
+вынос в микросервисы.
+
+```mermaid
+flowchart LR
+ Client([🌍 Клиент]) -->|"Accept: application/json; version=v1"| API
+
+ subgraph Django["🐍 Django Boilerplate"]
+ direction LR
+ API["🔌 API Layer
(serializers, views)"] --> Domain["🧠 Domain Layer
(services, selectors)"]
+ Domain --> Models["🗃️ Models"]
+ end
+
+ Models --> PG[("🐘 PostgreSQL")]
+ Domain --> RD[("⚡ Redis")]
+```
+
+## 📁 Структура проекта
+
+Структура следует лучшим практикам ([HackSoft Django Styleguide](https://github.com/HackSoftware/Django-Styleguide),
+[cookiecutter-django](https://github.com/cookiecutter/cookiecutter-django)) — три корневых пакета
+с чёткими зонами ответственности:
+
+```
+├── 📂 apps/ # Бизнес-приложения (изолированные модули)
+│ └── users/
+│ ├── api/ # Слой API: serializers, views, urls
+│ ├── services.py # Бизнес-логика (запись)
+│ ├── selectors.py # Бизнес-логика (чтение)
+│ ├── models.py
+│ └── tests/
+├── ⚙️ config/ # Только конфигурация проекта
+│ ├── settings/ # base.py + dev.py / prod.py (split-settings)
+│ ├── urls.py
+│ ├── asgi.py
+│ └── wsgi.py
+├── 🧰 core/ # Переиспользуемый инфраструктурный код
+│ ├── health.py # Health-check view
+│ ├── integrations/ # Клиенты внешних сервисов
+│ ├── mixins/ # Примеси для моделей (timestamps и т.п.)
+│ └── redis/ # Redis-клиент
+├── 🐳 Dockerfile # Multi-stage, non-root
+├── 🐳 docker-compose.yml # postgres + redis + django
+└── 📦 pyproject.toml # Зависимости (uv), ruff, bandit, pytest
+```
+
+| Пакет | Ответственность | Правило |
+| --- | --- | --- |
+| `apps/` | Бизнес-логика | Может импортировать `core/`, но не `config/` |
+| `core/` | Инфраструктура, переиспользуемый код | Не знает о бизнес-логике |
+| `config/` | Настройки, маршрутизация, WSGI/ASGI | Только конфигурация, без логики |
+
+## 🛠️ Команды
+
+```bash
+make install # 📦 установить зависимости (с dev-группой)
+make run # 🚀 запустить dev-сервер
+make migrate # 🗃️ применить миграции
+make makemigrations # 📝 создать миграции
+make superuser # 👑 создать суперпользователя
+make test # 🧪 запустить тесты
+make lint # 🔍 ruff check + format check + bandit
+make format # ✨ автоформатирование
+make up / down # 🐳 docker compose up/down
+```
+
+## ⚙️ Конфигурация
+
+Все настройки — через переменные окружения (см. [`.env.example`](.env.example)).
+
+
+📋 Ключевые переменные окружения
+
+
+
+| Переменная | Назначение | По умолчанию |
+| --- | --- | --- |
+| `ENVIRONMENT` | `dev` или `prod` (выбор настроек) | `dev` |
+| `DJANGO_SECRET_KEY` | Секретный ключ (обязателен) | — |
+| `DJANGO_DEBUG` | Режим отладки | `False` (prod), `True` (dev) |
+| `DJANGO_ALLOWED_HOSTS` | Разрешённые хосты через запятую | — |
+| `DATABASE_URL` | URL базы данных | SQLite |
+| `REDIS_URL` | URL Redis для кэша | `redis://redis:6379/1` |
+| `DJANGO_LOG_LEVEL` | Уровень логирования | `INFO` |
+| `GUNICORN_WORKERS` | Количество воркеров gunicorn | `4` |
+
+
+
+
+🔒 Production-развёртывание
+
+
+
+`ENVIRONMENT=prod` включает: `DEBUG=False`, SSL redirect, HSTS, secure cookies,
+JSON-only рендеринг DRF, манифестную статику (whitenoise) и запуск через gunicorn
+(`MODE=prod` в docker-compose).
+
+```bash
+ENVIRONMENT=prod MODE=prod INSTALL_DEV=false docker compose up -d --build
+```
+
+
+
+## 🧹 Качество кода
+
+```bash
+uv run pre-commit install # включить pre-commit хуки
+make lint # проверка
+make test # тесты
+```
+
+CI (GitHub Actions) запускает линтеры и тесты на каждый push в `main` и pull request.
+
+
+
+
+
+
diff --git a/apps/users/admin.py b/apps/users/admin.py
index d761427..638b75e 100644
--- a/apps/users/admin.py
+++ b/apps/users/admin.py
@@ -6,6 +6,8 @@
@admin.register(User)
class UserAdmin(BaseUserAdmin):
+ """Админ-панель для модели пользователя с email в качестве логина."""
+
ordering = ['email']
list_display = ['email', 'is_staff', 'is_active']
list_filter = ['is_staff', 'is_active']
diff --git a/apps/users/api/serializers.py b/apps/users/api/serializers.py
index 217a1d4..006a9f8 100644
--- a/apps/users/api/serializers.py
+++ b/apps/users/api/serializers.py
@@ -1,6 +1,8 @@
from rest_framework import serializers
+
from apps.users.models import User
+
class UserRegisterRequestSerializer(serializers.Serializer):
"""Сериализатор для валидации входящих данных при регистрации."""
@@ -26,6 +28,7 @@ class UserResponseSerializer(serializers.ModelSerializer):
Сериализатор для формирования ответа с данными пользователя.
Исключает чувствительные данные (пароли, права доступа).
"""
+
class Meta:
model = User
fields = ('external_id', 'email', 'date_joined', 'is_active')
diff --git a/apps/users/api/urls.py b/apps/users/api/urls.py
index 0c91361..a14548a 100644
--- a/apps/users/api/urls.py
+++ b/apps/users/api/urls.py
@@ -1,5 +1,6 @@
from django.urls import path
-from apps.users.api.views import UserRegisterApi, UserMeApi
+
+from apps.users.api.views import UserMeApi, UserRegisterApi
app_name = 'users_api'
urlpatterns = [
diff --git a/apps/users/api/views.py b/apps/users/api/views.py
index ba30562..8081349 100644
--- a/apps/users/api/views.py
+++ b/apps/users/api/views.py
@@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError
+from drf_spectacular.utils import extend_schema
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
@@ -16,8 +17,14 @@ class UserRegisterApi(APIView):
request_serializer = UserRegisterRequestSerializer
response_serializer = UserResponseSerializer
+ @extend_schema(
+ request=UserRegisterRequestSerializer,
+ responses={status.HTTP_201_CREATED: UserResponseSerializer},
+ summary='Регистрация нового пользователя',
+ )
def post(self, request: Request) -> Response:
- """Обрабатывает POST запрос на регистрацию.
+ """
+ Обрабатывает POST запрос на регистрацию.
1. Валидирует входящие данные через InputSerializer.
2. Вызывает сервис создания пользователя.
@@ -44,8 +51,13 @@ class UserMeApi(APIView):
permission_classes = [IsAuthenticated]
response_serializer = UserResponseSerializer
+ @extend_schema(
+ responses={status.HTTP_200_OK: UserResponseSerializer},
+ summary='Профиль текущего пользователя',
+ )
def get(self, request: Request) -> Response:
- """Возвращает данные текущего авторизованного пользователя.
+ """
+ Возвращает данные текущего авторизованного пользователя.
:param request: Объект HTTP запроса.
:return: HTTP ответ с данными профиля.
diff --git a/apps/users/apps.py b/apps/users/apps.py
index a173db1..5250607 100644
--- a/apps/users/apps.py
+++ b/apps/users/apps.py
@@ -2,5 +2,7 @@
class UsersConfig(AppConfig):
+ """Конфигурация приложения пользователей."""
+
name = 'apps.users'
verbose_name = 'Пользователи'
diff --git a/apps/users/managers.py b/apps/users/managers.py
index 09d987d..cd63bac 100644
--- a/apps/users/managers.py
+++ b/apps/users/managers.py
@@ -39,9 +39,9 @@ def create_superuser(self, email: str, password: str | None = None, **extra_fiel
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
- if extra_fields.get("is_staff") is not True:
- raise ValueError("Superuser must have is_staff=True.")
- if extra_fields.get("is_superuser") is not True:
- raise ValueError("Superuser must have is_superuser=True.")
+ if extra_fields.get('is_staff') is not True:
+ raise ValueError('Superuser must have is_staff=True.')
+ if extra_fields.get('is_superuser') is not True:
+ raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py
index fe6e3db..e2189d6 100644
--- a/apps/users/migrations/0001_initial.py
+++ b/apps/users/migrations/0001_initial.py
@@ -5,7 +5,6 @@
class Migration(migrations.Migration):
-
initial = True
dependencies = [
@@ -19,15 +18,47 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
- ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ (
+ 'is_superuser',
+ models.BooleanField(
+ default=False,
+ help_text='Designates that this user has all permissions without explicitly assigning them.',
+ verbose_name='superuser status',
+ ),
+ ),
('external_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
- ('slug', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Слаг страницы пользователя')),
+ (
+ 'slug',
+ models.CharField(
+ blank=True, max_length=255, null=True, unique=True, verbose_name='Слаг страницы пользователя'
+ ),
+ ),
('is_staff', models.BooleanField(default=False, verbose_name='Персонал')),
('is_active', models.BooleanField(default=True, verbose_name='Активный')),
('date_joined', models.DateTimeField(auto_now_add=True)),
- ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
- ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+ (
+ 'groups',
+ models.ManyToManyField(
+ blank=True,
+ help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
+ related_name='user_set',
+ related_query_name='user',
+ to='auth.group',
+ verbose_name='groups',
+ ),
+ ),
+ (
+ 'user_permissions',
+ models.ManyToManyField(
+ blank=True,
+ help_text='Specific permissions for this user.',
+ related_name='user_set',
+ related_query_name='user',
+ to='auth.permission',
+ verbose_name='user permissions',
+ ),
+ ),
],
options={
'verbose_name': 'Пользователь',
diff --git a/apps/users/selectors.py b/apps/users/selectors.py
index 8b6e4c1..78b845c 100644
--- a/apps/users/selectors.py
+++ b/apps/users/selectors.py
@@ -1,5 +1,6 @@
from apps.users.models import User
+
def get_user_by_email(*, email: str) -> User | None:
"""
Получает пользователя по email.
diff --git a/apps/users/tests/conftest.py b/apps/users/tests/conftest.py
index 54722fd..3b5ceee 100644
--- a/apps/users/tests/conftest.py
+++ b/apps/users/tests/conftest.py
@@ -30,8 +30,8 @@ def user_factory(
"""
def make_user(
- email: str = "user@example.com",
- password: str = "password",
+ email: str = 'user@example.com',
+ password: str = 'password',
**extra: Any,
) -> AbstractBaseUser:
"""
diff --git a/apps/users/tests/test_get_user_api_view.py b/apps/users/tests/test_get_user_api_view.py
index 02c8d43..141503c 100644
--- a/apps/users/tests/test_get_user_api_view.py
+++ b/apps/users/tests/test_get_user_api_view.py
@@ -4,7 +4,6 @@
from rest_framework import status
from rest_framework.test import APIClient
-
pytestmark = [pytest.mark.django_db]
@@ -15,7 +14,7 @@ def test_user_me_get_success(api_client: APIClient, user_factory: Any) -> None:
Arrange:
- Создаем пользователя.
- Аутентифицируем клиента этим пользователем.
- - Готовим GET-запрос к /api/v1/users/me/.
+ - Готовим GET-запрос к /api/users/me/.
Act:
- Выполняем запрос через APIClient.
@@ -27,19 +26,64 @@ def test_user_me_get_success(api_client: APIClient, user_factory: Any) -> None:
user = user_factory(email='exist@example.com', password='pwd123')
api_client.force_authenticate(user=user)
- response = api_client.get("/api/v1/users/me/")
+ response = api_client.get('/api/users/me/')
+
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data.get('email') == user.email
+
+
+def test_user_me_with_version_header(api_client: APIClient, user_factory: Any) -> None:
+ """
+ Проверяет работу версионирования API через заголовок Accept.
+
+ Arrange:
+ - Создаем пользователя и аутентифицируем клиента.
+ - Готовим GET-запрос с заголовком Accept: application/json; version=v1.
+
+ Act:
+ - Выполняем запрос через APIClient.
+
+ Assert:
+ - Проверяем HTTP-статус 200 (OK) для поддерживаемой версии.
+ """
+ user = user_factory(email='versioned@example.com', password='pwd123')
+
+ api_client.force_authenticate(user=user)
+ response = api_client.get('/api/users/me/', HTTP_ACCEPT='application/json; version=v1')
assert response.status_code == status.HTTP_200_OK
- assert response.data.get("email") == user.email
+ assert response.data.get('email') == user.email
+
+
+def test_user_me_with_unsupported_version_header(api_client: APIClient, user_factory: Any) -> None:
+ """
+ Проверяет отказ при запросе неподдерживаемой версии API в заголовке Accept.
+
+ Arrange:
+ - Создаем пользователя и аутентифицируем клиента.
+ - Готовим GET-запрос с заголовком Accept: application/json; version=v999.
+
+ Act:
+ - Выполняем запрос через APIClient.
+
+ Assert:
+ - Проверяем HTTP-статус 406 (Not Acceptable).
+ """
+ user = user_factory(email='unsupported@example.com', password='pwd123')
+
+ api_client.force_authenticate(user=user)
+ response = api_client.get('/api/users/me/', HTTP_ACCEPT='application/json; version=v999')
+
+ assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
-def test_user_me_unauthorized( api_client: APIClient) -> None:
+def test_user_me_unauthorized(api_client: APIClient) -> None:
"""
Проверяет запрет доступа к профилю для анонимного пользователя.
Arrange:
- Не аутентифицируем клиента.
- - Готовим GET-запрос к /api/v1/users/me/.
+ - Готовим GET-запрос к /api/users/me/.
Act:
- Выполняем запрос через APIClient.
@@ -47,7 +91,7 @@ def test_user_me_unauthorized( api_client: APIClient) -> None:
Assert:
- Проверяем HTTP-статус 401 или 403.
"""
- response = api_client.get("/api/v1/users/me/")
+ response = api_client.get('/api/users/me/')
assert response.status_code in (
status.HTTP_401_UNAUTHORIZED,
diff --git a/apps/users/tests/test_register_user_api_view.py b/apps/users/tests/test_register_user_api_view.py
index 262cef2..2e396b1 100644
--- a/apps/users/tests/test_register_user_api_view.py
+++ b/apps/users/tests/test_register_user_api_view.py
@@ -16,7 +16,7 @@ def test_user_register_success(
Arrange:
- Подготавливаем валидные данные (email, совпадающие пароли).
- - Формируем POST-запрос к /api/v1/users/register/.
+ - Формируем POST-запрос к /api/users/register/.
Act:
- Выполняем запрос через APIClient.
@@ -25,18 +25,18 @@ def test_user_register_success(
- Проверяем HTTP-статус 201 (Created).
- Проверяем, что возвращенный email соответствует переданному.
"""
- email = "newuser@example.com"
- password = "strongpassword"
+ email = 'newuser@example.com'
+ password = 'strongpassword'
payload = {
- "email": email,
- "password": password,
- "confirm_password": password,
+ 'email': email,
+ 'password': password,
+ 'confirm_password': password,
}
response = api_client.post(
- "/api/v1/users/register/",
+ '/api/users/register/',
data=payload,
- format="json",
+ format='json',
)
assert response.status_code == status.HTTP_201_CREATED
@@ -53,7 +53,7 @@ def test_user_register_service_validation_error(
Arrange:
- Подготавливаем данные для регистрации.
- Мокаем сервисный слой create_user на выброс ошибки.
- - Формируем POST-запрос к /api/v1/users/register/.
+ - Формируем POST-запрос к /api/users/register/.
Act:
- Выполняем запрос через APIClient.
@@ -63,21 +63,21 @@ def test_user_register_service_validation_error(
- Проверяем наличие сообщения об ошибке.
"""
mock_create_user = mocker.patch(
- "apps.users.api.views.create_user",
+ 'apps.users.api.views.create_user',
)
mock_create_user.side_effect = ValidationError('User already exists')
- password = "strongpassword"
+ password = 'strongpassword'
payload = {
- "email": "bad@example.com",
- "password": password,
- "confirm_password": password,
+ 'email': 'bad@example.com',
+ 'password': password,
+ 'confirm_password': password,
}
response = api_client.post(
- "/api/v1/users/register/",
+ '/api/users/register/',
data=payload,
- format="json",
+ format='json',
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -92,7 +92,7 @@ def test_user_register_password_mismatch(
Arrange:
- Подготавливаем данные с разными паролями.
- - Формируем запрос к /api/v1/users/register/.
+ - Формируем запрос к /api/users/register/.
Act:
- Выполняем запрос через APIClient.
@@ -101,15 +101,15 @@ def test_user_register_password_mismatch(
- Проверяем HTTP-статус 400 (Bad Request).
"""
payload = {
- "email": "test@example.com",
- "password": "password123",
- "confirm_password": "password456",
+ 'email': 'test@example.com',
+ 'password': 'password123',
+ 'confirm_password': 'password456',
}
response = api_client.post(
- "/api/v1/users/register/",
+ '/api/users/register/',
data=payload,
- format="json",
+ format='json',
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
diff --git a/apps/users/urls.py b/apps/users/urls.py
index 437f584..63d6d34 100644
--- a/apps/users/urls.py
+++ b/apps/users/urls.py
@@ -1,4 +1,2 @@
-from django.urls import path
-
app_name = 'users'
-urlpatterns = []
\ No newline at end of file
+urlpatterns = []
diff --git a/apps/users/views.py b/apps/users/views.py
index 91ea44a..60f00ef 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -1,3 +1 @@
-from django.shortcuts import render
-
# Create your views here.
diff --git a/config/settings/base.py b/config/settings/base.py
index 3d8b09f..4b663ad 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -5,7 +5,9 @@
env = environ.Env()
BASE_DIR = Path(__file__).resolve().parent.parent.parent
-environ.Env.read_env(BASE_DIR / '.env')
+
+if (BASE_DIR / '.env').exists():
+ environ.Env.read_env(BASE_DIR / '.env')
SECRET_KEY = env.str('DJANGO_SECRET_KEY')
@@ -13,6 +15,8 @@
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=[])
+CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=[])
+
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@@ -22,12 +26,15 @@
'django.contrib.staticfiles',
# Dependency apps
'rest_framework',
+ 'django_filters',
+ 'drf_spectacular',
# Local apps
'apps.users',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
+ 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -56,11 +63,12 @@
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
- },
+ 'default': env.db('DATABASE_URL', default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'),
}
+DATABASES['default']['CONN_MAX_AGE'] = env.int('DJANGO_DB_CONN_MAX_AGE', default=60)
+DATABASES['default']['CONN_HEALTH_CHECKS'] = True
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'users.User'
@@ -88,12 +96,78 @@
USE_TZ = True
STATIC_URL = 'static/'
+STATIC_ROOT = env.path('DJANGO_STATIC_ROOT', default=BASE_DIR / 'staticfiles')
+
+STORAGES = {
+ 'default': {
+ 'BACKEND': 'django.core.files.storage.FileSystemStorage',
+ },
+ 'staticfiles': {
+ 'BACKEND': 'whitenoise.storage.CompressedStaticFilesStorage',
+ },
+}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
+ 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
+ # Версионирование API через заголовок Accept:
+ # Accept: application/json; version=v1
+ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
+ 'DEFAULT_VERSION': 'v1',
+ 'ALLOWED_VERSIONS': ('v1',),
+ 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
+ 'PAGE_SIZE': 20,
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+ 'DEFAULT_THROTTLE_CLASSES': ('rest_framework.throttling.AnonRateThrottle',),
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': env.str('DJANGO_THROTTLE_ANON', default='100/min'),
+ },
+}
+
+SPECTACULAR_SETTINGS = {
+ 'TITLE': 'Django Boilerplate API',
+ 'DESCRIPTION': ('Версия API передаётся через заголовок Accept, например: `Accept: application/json; version=v1`.'),
+ 'VERSION': '1.0.0',
+ 'SERVE_INCLUDE_SCHEMA': False,
+}
+
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+ 'LOCATION': env.str('REDIS_URL', default='redis://redis:6379/1'),
+ },
+}
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'verbose': {
+ 'format': '{asctime} {levelname} {name} {module}:{lineno} {message}',
+ 'style': '{',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose',
+ },
+ },
+ 'root': {
+ 'handlers': ['console'],
+ 'level': env.str('DJANGO_LOG_LEVEL', default='INFO'),
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['console'],
+ 'level': env.str('DJANGO_LOG_LEVEL', default='INFO'),
+ 'propagate': False,
+ },
+ },
}
REDIS_HOST: str = env.str('REDIS_HOST', 'redis')
diff --git a/config/settings/dev.py b/config/settings/dev.py
index 0008700..52bb01e 100644
--- a/config/settings/dev.py
+++ b/config/settings/dev.py
@@ -1 +1,19 @@
-# Development settings
\ No newline at end of file
+# Development settings
+DEBUG = env.bool('DJANGO_DEBUG', default=True) # noqa: F821
+
+if not ALLOWED_HOSTS: # noqa: F821
+ ALLOWED_HOSTS = ['localhost', '127.0.0.1']
+
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+# Whitenoise отдаёт статику напрямую из finders без collectstatic
+WHITENOISE_AUTOREFRESH = True
+WHITENOISE_USE_FINDERS = True
+
+# Локальная разработка без Redis: кэш в памяти процесса
+if env.bool('DJANGO_USE_LOCMEM_CACHE', default=False): # noqa: F821
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ },
+ }
diff --git a/config/settings/prod.py b/config/settings/prod.py
index e6fd2af..1a40ac0 100644
--- a/config/settings/prod.py
+++ b/config/settings/prod.py
@@ -1 +1,24 @@
-# Production settings
\ No newline at end of file
+# Production settings
+DEBUG = False
+
+# Приложение работает за обратным прокси (nginx/traefik), который терминирует TLS
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+SECURE_SSL_REDIRECT = env.bool('DJANGO_SECURE_SSL_REDIRECT', default=True) # noqa: F821
+
+SECURE_HSTS_SECONDS = env.int('DJANGO_SECURE_HSTS_SECONDS', default=60 * 60 * 24 * 30) # noqa: F821
+SECURE_HSTS_INCLUDE_SUBDOMAINS = True
+SECURE_HSTS_PRELOAD = True
+
+SESSION_COOKIE_SECURE = True
+CSRF_COOKIE_SECURE = True
+SESSION_COOKIE_HTTPONLY = True
+
+SECURE_CONTENT_TYPE_NOSNIFF = True
+SECURE_REFERRER_POLICY = 'same-origin'
+X_FRAME_OPTIONS = 'DENY'
+
+# Манифестное хранилище статики: хэши в именах файлов + сжатие
+STORAGES['staticfiles']['BACKEND'] = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # noqa: F821
+
+# В production браузерные формы DRF не нужны
+REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ('rest_framework.renderers.JSONRenderer',) # noqa: F821
diff --git a/config/urls.py b/config/urls.py
index 2c177f9..799f8d6 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -1,21 +1,20 @@
"""
-URL configuration for config project.
+URL configuration проекта.
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/6.0/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.urls import include, path
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+Версионирование API реализовано через заголовок Accept (AcceptHeaderVersioning),
+поэтому версия не входит в URL. Клиент указывает версию так:
+
+ Accept: application/json; version=v1
+
+Если заголовок не передан, используется версия по умолчанию (см. REST_FRAMEWORK
+в config/settings/base.py).
"""
+
from django.contrib import admin
-from django.urls import path, include
+from django.urls import include, path
+from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
+
+from core.health import health_check
api_urlpatterns = [
path('users/', include('apps.users.api.urls', namespace='users_api')),
@@ -23,5 +22,10 @@
urlpatterns = [
path('admin/', admin.site.urls),
- path('api/v1/', include(api_urlpatterns)),
+ path('health/', health_check, name='health_check'),
+ path('api/', include(api_urlpatterns)),
+ # OpenAPI schema & docs
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
+ path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
+ path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
diff --git a/commons/__init__.py b/core/__init__.py
similarity index 100%
rename from commons/__init__.py
rename to core/__init__.py
diff --git a/core/health.py b/core/health.py
new file mode 100644
index 0000000..d6fcdad
--- /dev/null
+++ b/core/health.py
@@ -0,0 +1,30 @@
+from django.db import connections
+from django.http import HttpRequest, JsonResponse
+
+from core.redis import get_redis
+
+
+def health_check(request: HttpRequest) -> JsonResponse:
+ """
+ Проверяет работоспособность приложения и его зависимостей (БД, Redis).
+
+ :param request: Объект HTTP запроса.
+ :return: JSON со статусом каждого компонента; 503, если хоть один недоступен.
+ """
+ checks = {'database': True, 'redis': True}
+
+ try:
+ connections['default'].cursor().execute('SELECT 1')
+ except Exception: # noqa: BLE001
+ checks['database'] = False
+
+ try:
+ get_redis().ping()
+ except Exception: # noqa: BLE001
+ checks['redis'] = False
+
+ healthy = all(checks.values())
+ return JsonResponse(
+ {'status': 'ok' if healthy else 'error', 'checks': checks},
+ status=200 if healthy else 503,
+ )
diff --git a/commons/mixins/__init__.py b/core/integrations/__init__.py
similarity index 100%
rename from commons/mixins/__init__.py
rename to core/integrations/__init__.py
diff --git a/externals/__init__.py b/core/mixins/__init__.py
similarity index 100%
rename from externals/__init__.py
rename to core/mixins/__init__.py
diff --git a/commons/mixins/models.py b/core/mixins/models.py
similarity index 94%
rename from commons/mixins/models.py
rename to core/mixins/models.py
index 4d4ea18..9c7b82d 100644
--- a/commons/mixins/models.py
+++ b/core/mixins/models.py
@@ -8,4 +8,4 @@ class AutoTimestampMixin(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name='Дата обновления')
class Meta:
- abstract = True
\ No newline at end of file
+ abstract = True
diff --git a/commons/redis/__init__.py b/core/redis/__init__.py
similarity index 100%
rename from commons/redis/__init__.py
rename to core/redis/__init__.py
diff --git a/commons/redis/keys.py b/core/redis/keys.py
similarity index 100%
rename from commons/redis/keys.py
rename to core/redis/keys.py
diff --git a/docker-compose.yml b/docker-compose.yml
index 1b85b8c..e86aae9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,27 @@
services:
+ postgres:
+ image: postgres:18-alpine
+ hostname: postgres
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_DB: ${POSTGRES_DB:-app}
+ POSTGRES_USER: ${POSTGRES_USER:-app}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app}
+ volumes:
+ - pg_data:/var/lib/postgresql/data
+ healthcheck:
+ test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}" ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+ restart: unless-stopped
+ networks:
+ - backend
+
redis:
- image: redis:7-alpine
+ image: redis:8-alpine
hostname: redis
ports:
- "6379:6379"
@@ -27,7 +48,7 @@ services:
dockerfile: Dockerfile
target: runtime
args:
- PYTHON_VERSION: ${PYTHON_VERSION:-3.13.7}
+ PYTHON_VERSION: ${PYTHON_VERSION:-3.13}
UID: ${UID:-1000}
INSTALL_DEV: ${INSTALL_DEV:-true}
hostname: django
@@ -41,9 +62,14 @@ services:
environment:
ENVIRONMENT: ${ENVIRONMENT:-dev}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-config.settings}
+ DATABASE_URL: postgres://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-app}@postgres:5432/${POSTGRES_DB:-app}
+ REDIS_URL: redis://redis:6379/1
+ POSTGRES_HOST: postgres
env_file:
- ./.env
depends_on:
+ postgres:
+ condition: service_healthy
redis:
condition: service_healthy
command: ${MODE:-dev}
diff --git a/docs/styleguide/api_view.md b/docs/styleguide/api_view.md
deleted file mode 100644
index f19810a..0000000
--- a/docs/styleguide/api_view.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Единый HTTP-слой на APIView
-
-## Цели
-
-- Единообразный стиль реализации HTTP-слоя.
-- Прозрачное разделение обязанностей между APIView и доменным слоем (services/selectors).
-- Упрощение чтения и сопровождения кода для всей команды.
-
-***
-
-## Почему APIView
-
-В проекте используется единый подход: все HTTP-эндпоинты реализуются на базе `rest_framework.views.APIView`, а не через Function-Based Views или GenericViewSet/ModelViewSet.
-
-Плюсы такого подхода:
-
-- Явный и предсказуемый жизненный цикл запроса (`dispatch`, `get/post/...`).
-- Полный контроль над тем, как вызываются сериализаторы, сервисы и селекторы.
-- Отсутствие скрытой магии generic-классов, проще читать и отлаживать код.
-- Единый паттерн для всех разработчиков - меньше когнитивной нагрузки при переходе между модулями.
-
-***
-
-## Общие правила для APIView
-
-1. Один публичный HTTP-метод - одна основная операция (например, `post` для создания, `get` для получения).
-2. В APIView запрещено:
- - Писать бизнес-логику.
- - Работать напрямую с моделями (кроме простых случаев чтения id из URL).
- - Дублировать логику валидации данных, уже реализованную в сериализаторах или сервисах.
-
-3. В APIView разрешено и рекомендуется:
- - Делегировать доменную логику в `services` и `selectors`.
- - Использовать сериализаторы только для валидации входных данных и формирования ответа.
- - Обрабатывать только HTTP-аспекты: статус-коды, формат ответа, маппинг исключений.
-
-***
-
-## Структура APIView
-
-Каждый метод (`get`, `post`, `put`, `patch`, `delete`) логически делится на 4 шага:
-
-1. Извлечение и валидация входных данных (`RequestSerializer` / query params / path params).
-2. Вызов селектора/сервиса.
-3. Преобразование результата в `ResponseSerializer`.
-4. Формирование `Response` с корректным HTTP-статусом.
-
-Пример шаблона:
-
-```python
-from typing import Any
-
-from django.core.exceptions import ValidationError
-from rest_framework import status
-from rest_framework.request import Request
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from apps.users.api.serializers import UserRegisterRequestSerializer, UserResponseSerializer
-from apps.users.services import create_user
-
-
-class UserRegisterApi(APIView):
- """
- Регистрация нового пользователя.
- """
-
- def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
- """
- Обрабатывает POST-запрос на регистрацию пользователя.
-
- :param request: HTTP-запрос
- :return: HTTP-ответ с данными пользователя или ошибкой
- """
- # 1. Валидация входа
- serializer = UserRegisterRequestSerializer(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- # 2. Вызов сервиса
- try:
- user = create_user(
- email=serializer.validated_data['email'],
- password=serializer.validated_data['password'],
- )
- except ValidationError as exc:
- return Response(
- {'detail': str(exc)},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # 3. Сериализация результата
- output_serializer = UserResponseSerializer(user)
-
- # 4. Формирование ответа
- return Response(
- output_serializer.data,
- status=status.HTTP_201_CREATED,
- )
-```
-
-***
-
-## Требования к сериализаторам для APIView
-
-Для единообразия:
-
-- Используются отдельные сериализаторы для:
- - входных данных: `*RequestSerializer`,
- - выходных данных: `*ResponseSerializer`, если это оправдано.
-- APIView не должна "догадываться", какие поля можно принимать, это описано в `RequestSerializer`.
-- APIView не должна собирать response-JSON вручную - это обязанность `ResponseSerializer`.
-
-***
-
-## Тестирование APIView
-
-- Для всех APIView пишутся интеграционные тесты через `APIClient`.
-- Структура тестов:
- - docstring в формате AAA (Arrange / Act / Assert).
- - Проверка:
- - кодов ответов,
- - структуры и содержания ответа,
- - взаимодействия с сервисами (при необходимости через моки).
-
-Пример:
-
-```python
-from typing import Any
-
-from rest_framework import status
-from rest_framework.test import APIClient
-
-
-def test_user_register_success(api_client: APIClient) -> None:
- """
- Проверяет успешную регистрацию пользователя.
-
- Arrange:
- - Подготавливаем валидные данные.
- - Формируем POST-запрос к /api/v1/users/register/.
-
- Act:
- - Выполняем запрос через APIClient.
-
- Assert:
- - Статус 201.
- - Email в ответе совпадает с переданным.
- """
- payload = {
- 'email': 'new@example.com',
- 'password': 'pwd123',
- 'confirm_password': 'pwd123',
- }
-
- response = api_client.post(
- '/api/v1/users/register/',
- data=payload,
- format='json',
- )
-
- assert response.status_code == status.HTTP_201_CREATED
- assert response.data['email'] == 'new@example.com'
-```
-
-***
-
-## Организация кода в приложении
-
-Рекомендуемая структура модуля приложения:
-
-```text
-apps/
- users/
- api/
- views.py # APIView-контроллеры
- serializers.py # Request/Response сериализаторы
- urls.py # Роутинг API
- services.py # Бизнес-логика (изменение состояния)
- selectors.py # Чтение и выборки
- models.py
- tests/
- test_api_*.py
- test_services_*.py
- test_selectors_*.py
-```
\ No newline at end of file
diff --git a/docs/styleguide/docstring.md b/docs/styleguide/docstring.md
deleted file mode 100644
index 3143f48..0000000
--- a/docs/styleguide/docstring.md
+++ /dev/null
@@ -1,93 +0,0 @@
-## Общие правила для docstring и комментариев
-
-- В теле функций и тестов использовать одинарные кавычки `'...'`, кроме docstring.
-- Многострочный docstring:
- - Открывающие `"""` на отдельной строке.
- - Со следующей строки - краткое описание.
-
-Пример:
-
-```python
-def do_something(x: int) -> int:
- """
- Делает что-то с числом.
-
- :param x: входное значение
- :return: результат обработки
- """
- result = x + 1
- return result
-```
-
-## Docstring для обычного кода
-
-- Не использовать AAA.
-- Использовать reST-нотацию с `:param`, `:return`, `:raises` при необходимости.
-- Если функция ничего не возвращает, `:return` можно опускать.
-- Если функция может выбрасывать исключения - документировать через `:raises`.
-
-Пример сервиса:
-
-```python
-def create_user(email: str, password: str) -> User:
- """
- Создает нового пользователя в системе.
-
- :param email: адрес электронной почты пользователя
- :param password: пароль пользователя в открытом виде
- :return: созданный пользователь
- :raises ValueError: если email уже занят или некорректен
- """
- if not email:
- raise ValueError('Email is required')
-
- user = User.objects.create_user(email=email, password=password)
- return user
-```
-
-## Комментарии в коде
-
-- Не писать комментарии для очевидных вещей.
-- Комментарии добавляются только по отдельному запросу или для пояснения нетривиальной логики.
-- Предпочтение - чистый код и качественные docstring вместо обилия комментариев.
-
-## Кавычки
-
-- В теле функций и тестов - одинарные кавычки `'...'`.
-- В docstring - тройные двойные кавычки `"""..."""`.
-
-## Docstring для тестов
-
-- Всегда использовать формат AAA:
- - `Arrange:` - подготовка данных и окружения.
- - `Act:` - одно действие, которое тестируется.
- - `Assert:` - проверки результата.
-- Описание теста кратко формулирует, что именно проверяется.
-
-Пример:
-
-```python
-def test_user_me_get_success(api_client: APIClient, user_factory: Any) -> None:
- """
- Проверяет успешное получение профиля текущего пользователя.
-
- Arrange:
- - Создаем пользователя.
- - Аутентифицируем клиента.
-
- Act:
- - Выполняем GET-запрос к /api/v1/users/me/.
-
- Assert:
- - Проверяем статус 200.
- - Проверяем, что email в ответе совпадает с ожидаемым.
- """
- email = 'user@example.com'
- user = user_factory(email=email, password='password123')
-
- api_client.force_authenticate(user=user)
- response = api_client.get('/api/v1/users/me/')
-
- assert response.status_code == status.HTTP_200_OK
- assert response.data['email'] == email
-```
\ No newline at end of file
diff --git a/docs/styleguide/services_and_selectors.md b/docs/styleguide/services_and_selectors.md
deleted file mode 100644
index 9573125..0000000
--- a/docs/styleguide/services_and_selectors.md
+++ /dev/null
@@ -1,297 +0,0 @@
-# Services и Selectors в Django/DRF
-
-## Цели
-
-- Явное разделение чтения и изменения состояния.
-- Упрощение тестирования и повторного использования логики.
-- "Тонкие" контроллеры (APIView/ViewSet), "толстый" домен (services/selectors).
-
-***
-
-## Selectors
-
-### Назначение
-
-Selectors отвечают только за чтение данных:
-
-- Не изменяют состояние БД (никаких `.save()`, `.create()`, `.delete()`).
-- Инкапсулируют логику выборок и оптимизации запросов (фильтры, `select_related`, `prefetch_related`).
-- Возвращают:
- - `QuerySet`,
- - конкретные модели,
- - DTO/словари/агрегированные структуры.
-
-### Пример
-
-```python
-# apps/users/selectors.py
-
-from typing import Iterable
-
-from django.db.models import QuerySet
-
-from apps.users.models import User
-
-
-def get_active_users() -> QuerySet[User]:
- """
- Возвращает queryset активных пользователей.
- """
- return User.objects.filter(is_active=True)
-
-
-def get_user_by_email(email: str) -> User | None:
- """
- Возвращает пользователя по email или None.
-
- :param email: адрес электронной почты
- :return: пользователь или None, если не найден
- """
- return User.objects.filter(email=email).first()
-```
-
-***
-
-## Services
-
-### Назначение
-
-Services отвечают за изменение состояния и бизнес-правила:
-
-- Создание, обновление, удаление сущностей.
-- Внешние интеграции (HTTP-клиенты, email/SMS, очереди задач).
-- Доменные проверки и бизнес-валидация (ограничения, workflow).
-
-APIView не должна содержать бизнес-логику; она вызывает сервис и мапит результат в HTTP-ответ.
-
-### Пример
-
-```python
-# apps/users/services.py
-
-from django.core.exceptions import ValidationError
-
-from apps.users.models import User
-
-
-def create_user(email: str, password: str) -> User:
- """
- Создает нового пользователя.
-
- :param email: адрес электронной почты
- :param password: пароль пользователя в открытом виде
- :return: созданный пользователь
- :raises ValidationError: если пользователь с таким email уже существует
- """
- if User.objects.filter(email=email).exists():
- raise ValidationError('User with this email already exists')
-
- user = User.objects.create_user(email=email, password=password)
- return user
-```
-
-***
-
-## Связка APIView → Services/Selectors
-
-### Принципы
-
-APIView/ViewSet выполняет только:
-
-- Авторизацию, permissions, throttling.
-- Валидацию входа через сериализатор.
-- Вызов нужного `service` или `selector`.
-- Превращение результата в сериализатор/JSON.
-- Маппинг исключений домена в HTTP-коды.
-
-### Пример APIView
-
-```python
-# apps/users/api/views.py
-
-from typing import Any
-
-from django.core.exceptions import ValidationError
-from rest_framework import status
-from rest_framework.request import Request
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from apps.users.api.serializers import UserRegisterInputSerializer, UserOutputSerializer
-from apps.users.services import create_user
-
-
-class UserRegisterApi(APIView):
- """
- Регистрация нового пользователя.
- """
-
- def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
- """
- Обрабатывает POST-запрос на регистрацию.
-
- :param request: HTTP-запрос
- :return: HTTP-ответ с данными пользователя или ошибкой
- """
- serializer = UserRegisterInputSerializer(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- try:
- user = create_user(
- email=serializer.validated_data['email'],
- password=serializer.validated_data['password'],
- )
- except ValidationError as exc:
- return Response(
- {'detail': str(exc)},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- output = UserOutputSerializer(user)
- return Response(output.data, status=status.HTTP_201_CREATED)
-```
-
-***
-
-## Тестирование
-
-### Selectors
-
-- Чистые unit-тесты без HTTP.
-- Проверяем:
- - корректность фильтров и результата,
- - количество запросов (при необходимости),
- - граничные случаи (нет записей, несколько записей).
-
-Пример:
-
-```python
-def test_get_active_users_returns_only_active_users(django_user_model) -> None:
- """
- Проверяет, что возвращаются только активные пользователи.
-
- Arrange:
- - Создаем активного и неактивного пользователей.
-
- Act:
- - Вызываем get_active_users.
-
- Assert:
- - В результирующем queryset только активные пользователи.
- """
- django_user_model.objects.create_user(email='a@example.com', password='pwd', is_active=True)
- django_user_model.objects.create_user(email='b@example.com', password='pwd', is_active=False)
-
- qs = get_active_users()
-
- emails = {u.email for u in qs}
- assert emails == {'a@example.com'}
-```
-
-### Services
-
-- Unit-тесты на уровне Python/ORM.
-- Мокаем внешние интеграции.
-- Проверяем:
- - эффект в БД,
- - выбрасываемые исключения,
- - вызовы внешних сервисов.
-
-Пример:
-
-```python
-def test_create_user_success(django_user_model) -> None:
- """
- Проверяет успешное создание пользователя.
-
- Arrange:
- - Подготавливаем email и пароль.
-
- Act:
- - Вызываем create_user.
-
- Assert:
- - Пользователь создается в БД.
- """
- email = 'test@example.com'
- password = 'pwd123'
-
- user = create_user(email=email, password=password)
-
- assert user.email == email
- assert django_user_model.objects.filter(email=email).exists()
-```
-
-### APIView
-
-- Интеграционные тесты через `APIClient`.
-- Проверяем:
- - auth/permissions,
- - 200/201 vs 400/401/403/409,
- - маппинг исключений сервиса в HTTP.
-- При необходимости мокаем `services` в негативных сценариях.
-
-Пример:
-
-```python
-from rest_framework import status
-from rest_framework.test import APIClient
-
-
-def test_user_register_success(api_client: APIClient) -> None:
- """
- Проверяет успешную регистрацию пользователя.
-
- Arrange:
- - Подготавливаем валидные данные.
- - Формируем POST-запрос к /api/v1/users/register/.
-
- Act:
- - Выполняем запрос через APIClient.
-
- Assert:
- - Статус 201.
- - Email в ответе совпадает с переданным.
- """
- payload = {
- 'email': 'new@example.com',
- 'password': 'pwd123',
- 'confirm_password': 'pwd123',
- }
-
- response = api_client.post(
- '/api/v1/users/register/',
- data=payload,
- format='json',
- )
-
- assert response.status_code == status.HTTP_201_CREATED
- assert response.data['email'] == 'new@example.com'
-```
-
-***
-
-## Структура проекта
-
-Рекомендуемая структура для каждого приложения:
-
-```text
-apps/
- users/
- api/
- views.py
- serializers.py
- urls.py
- services.py
- selectors.py
- models.py
- tests/
- test_api_*.py
- test_services_*.py
- test_selectors_*.py
-```
-
-- `api/` – HTTP-слой (DRF).
-- `services.py` – изменение состояния и бизнес-логика.
-- `selectors.py` – чтение и выборки.
-- `tests/` разделяются по тем же слоям.
\ No newline at end of file
diff --git a/entrypoint.sh b/entrypoint.sh
index 6c7b42f..f00de56 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,11 +1,7 @@
#!/bin/bash
set -e
-# Проверка виртуального окружения
-echo "Проверка Python окружения:"
-echo "Python path: $(which python)"
-echo "Python version: $(python --version)"
-echo "Virtual env: ${VIRTUAL_ENV:-'Используется PATH для активации venv'}"
+echo "Python: $(python --version) ($(which python))"
# Функция для ожидания готовности сервисов
wait_for_service() {
@@ -20,9 +16,13 @@ wait_for_service() {
echo "$service готов!"
}
-# Ожидание Redis если он используется
+# Ожидание зависимостей
if [ -n "$REDIS_URL" ]; then
- wait_for_service redis 6379 "Redis"
+ wait_for_service "${REDIS_HOST:-redis}" "${REDIS_PORT:-6379}" "Redis"
+fi
+
+if [ -n "$POSTGRES_HOST" ]; then
+ wait_for_service "$POSTGRES_HOST" "${POSTGRES_PORT:-5432}" "PostgreSQL"
fi
# Применение миграций
@@ -30,7 +30,7 @@ echo "Применение миграций..."
python manage.py migrate --no-input
# Сбор статических файлов в продакшене
-if [ "$MODE" = "prod" ] || [ "$DEBUG" = "False" ]; then
+if [ "$1" = "prod" ] || [ "$ENVIRONMENT" = "prod" ]; then
echo "Сбор статических файлов..."
python manage.py collectstatic --no-input --clear
fi
@@ -43,11 +43,9 @@ case "$1" in
;;
"prod")
echo "Запуск в продакшн режиме..."
- exec gunicorn core.wsgi:application \
+ exec gunicorn config.wsgi:application \
--bind 0.0.0.0:8000 \
- --workers 4 \
- --worker-class gevent \
- --worker-connections 1000 \
+ --workers "${GUNICORN_WORKERS:-4}" \
--max-requests 1000 \
--max-requests-jitter 100 \
--timeout 30 \
@@ -56,23 +54,6 @@ case "$1" in
--access-logfile - \
--error-logfile -
;;
- "celery-worker")
- echo "Запуск Celery Worker..."
- exec celery -A core worker \
- --loglevel=info \
- --concurrency=4 \
- --max-tasks-per-child=1000 \
- --time-limit=300 \
- --soft-time-limit=240
- ;;
- "celery-beat")
- echo "Запуск Celery Beat..."
- # Удаление старого расписания если есть
- rm -f celerybeat.pid
- exec celery -A core beat \
- --loglevel=info \
- --scheduler django_celery_beat.schedulers:DatabaseScheduler
- ;;
"shell")
echo "Запуск Django Shell..."
exec python manage.py shell
diff --git a/manage.py b/manage.py
index 8e7ac79..5e1dd6b 100755
--- a/manage.py
+++ b/manage.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
+
import os
import sys
@@ -12,8 +13,8 @@ def main():
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
- "available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
+ 'available on your PYTHONPATH environment variable? Did you '
+ 'forget to activate a virtual environment?'
) from exc
execute_from_command_line(sys.argv)
diff --git a/pyproject.toml b/pyproject.toml
index 916cfd4..31da175 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,23 +5,28 @@ description = "Add your description here"
readme = "README.md"
requires-python = "==3.13.*"
dependencies = [
- "django==6.0",
- "django-environ==0.12.0",
+ "django==6.0.6",
+ "django-environ==0.13.0",
"django-filter==25.2",
"django-split-settings==1.3.2",
- "djangorestframework==3.16.1",
- "redis[hiredis]==7.1.0",
+ "djangorestframework==3.17.1",
+ "drf-spectacular==0.29.0",
+ "gunicorn==26.0.0",
+ "psycopg[binary,pool]==3.3.4",
+ "redis[hiredis]==8.0.0",
+ "whitenoise==6.12.0",
]
[dependency-groups]
dev = [
- "bandit==1.9.2",
- "pre-commit==4.5.0",
- "pytest==9.0.1",
- "pytest-django==4.11.1",
+ "bandit==1.9.4",
+ "pre-commit==4.6.0",
+ "pytest==9.0.3",
+ "pytest-django==4.12.0",
+ "pytest-env==1.6.0",
"pytest-mock==3.15.1",
- "ruff==0.14.7",
- "ty==0.0.1a34",
+ "ruff==0.15.17",
+ "ty==0.0.49",
]
[tool.ruff]
@@ -62,3 +67,11 @@ exclude_dirs = ['.venv', 'migrations', '__pycache__', 'tests']
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = 'config.settings'
python_files = ['test_*.py', '*_test.py']
+# Значения по умолчанию для запуска тестов без .env (локально и в CI).
+# Префикс D: — не перезаписывать уже установленные переменные окружения.
+env = [
+ 'D:ENVIRONMENT=dev',
+ 'D:DJANGO_SECRET_KEY=insecure-test-secret-key',
+ 'D:DJANGO_DEBUG=True',
+ 'D:DJANGO_USE_LOCMEM_CACHE=True',
+]
diff --git a/uv.lock b/uv.lock
index 396de91..41607e9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,19 +1,28 @@
version = 1
-revision = 1
+revision = 3
requires-python = "==3.13.*"
[[package]]
name = "asgiref"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969 }
+sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096 },
+ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
name = "bandit"
-version = "1.9.2"
+version = "1.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -21,50 +30,50 @@ dependencies = [
{ name = "rich" },
{ name = "stevedore" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/cf/72/f704a97aac430aeb704fa16435dfa24fbeaf087d46724d0965eb1f756a2c/bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce", size = 4241659 }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/55/1a/5b0320642cca53a473e79c7d273071b5a9a8578f9e370b74da5daa2768d7/bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868", size = 134377 },
+ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
]
[[package]]
name = "cfgv"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334 }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445 },
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 },
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "django"
-version = "6.0"
+version = "6.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/15/75/19762bfc4ea556c303d9af8e36f0cd910ab17dff6c8774644314427a2120/django-6.0.tar.gz", hash = "sha256:7b0c1f50c0759bbe6331c6a39c89ae022a84672674aeda908784617ef47d8e26", size = 10932418 }
+sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d7/ae/f19e24789a5ad852670d6885f5480f5e5895576945fcc01817dfd9bc002a/django-6.0-py3-none-any.whl", hash = "sha256:1cc2c7344303bbfb7ba5070487c17f7fc0b7174bbb0a38cebf03c675f5f19b6d", size = 8339181 },
+ { url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" },
]
[[package]]
@@ -77,7 +86,11 @@ dependencies = [
{ name = "django-filter" },
{ name = "django-split-settings" },
{ name = "djangorestframework" },
+ { name = "drf-spectacular" },
+ { name = "gunicorn" },
+ { name = "psycopg", extra = ["binary", "pool"] },
{ name = "redis", extra = ["hiredis"] },
+ { name = "whitenoise" },
]
[package.dev-dependencies]
@@ -86,6 +99,7 @@ dev = [
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-django" },
+ { name = "pytest-env" },
{ name = "pytest-mock" },
{ name = "ruff" },
{ name = "ty" },
@@ -93,32 +107,37 @@ dev = [
[package.metadata]
requires-dist = [
- { name = "django", specifier = "==6.0" },
- { name = "django-environ", specifier = "==0.12.0" },
+ { name = "django", specifier = "==6.0.6" },
+ { name = "django-environ", specifier = "==0.13.0" },
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-split-settings", specifier = "==1.3.2" },
- { name = "djangorestframework", specifier = "==3.16.1" },
- { name = "redis", extras = ["hiredis"], specifier = "==7.1.0" },
+ { name = "djangorestframework", specifier = "==3.17.1" },
+ { name = "drf-spectacular", specifier = "==0.29.0" },
+ { name = "gunicorn", specifier = "==26.0.0" },
+ { name = "psycopg", extras = ["binary", "pool"], specifier = "==3.3.4" },
+ { name = "redis", extras = ["hiredis"], specifier = "==8.0.0" },
+ { name = "whitenoise", specifier = "==6.12.0" },
]
[package.metadata.requires-dev]
dev = [
- { name = "bandit", specifier = "==1.9.2" },
- { name = "pre-commit", specifier = "==4.5.0" },
- { name = "pytest", specifier = "==9.0.1" },
- { name = "pytest-django", specifier = "==4.11.1" },
+ { name = "bandit", specifier = "==1.9.4" },
+ { name = "pre-commit", specifier = "==4.6.0" },
+ { name = "pytest", specifier = "==9.0.3" },
+ { name = "pytest-django", specifier = "==4.12.0" },
+ { name = "pytest-env", specifier = "==1.6.0" },
{ name = "pytest-mock", specifier = "==3.15.1" },
- { name = "ruff", specifier = "==0.14.7" },
- { name = "ty", specifier = "==0.0.1a34" },
+ { name = "ruff", specifier = "==0.15.17" },
+ { name = "ty", specifier = "==0.0.49" },
]
[[package]]
name = "django-environ"
-version = "0.12.0"
+version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804 }
+sdist = { url = "https://files.pythonhosted.org/packages/18/3c/60983e6ec9b24a8d8588eecebfd21123cba980bce0a905807a27692f0860/django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f", size = 63529, upload-time = "2026-02-18T01:08:08.791Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957 },
+ { url = "https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e", size = 20682, upload-time = "2026-02-18T01:08:07.359Z" },
]
[[package]]
@@ -128,78 +147,143 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818 }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145 },
+ { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
]
[[package]]
name = "django-split-settings"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/de/b9/1c13089454afd7a42d492b8aa8a0c7e49eeca58c0f2ad331f361a067c876/django_split_settings-1.3.2.tar.gz", hash = "sha256:d3975aa3601e37f65c59b9977b6bcb1de8bc27496930054078589c7d56998a9d", size = 5751 }
+sdist = { url = "https://files.pythonhosted.org/packages/de/b9/1c13089454afd7a42d492b8aa8a0c7e49eeca58c0f2ad331f361a067c876/django_split_settings-1.3.2.tar.gz", hash = "sha256:d3975aa3601e37f65c59b9977b6bcb1de8bc27496930054078589c7d56998a9d", size = 5751, upload-time = "2024-07-05T14:30:05.997Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/69/d94db8dac55bcfb6b3243578a3096cfda6c42ea5da292c36919768152ec6/django_split_settings-1.3.2-py3-none-any.whl", hash = "sha256:72bd7dd9f12602585681074d1f859643fb4f6b196b584688fab86bdd73a57dff", size = 6435 },
+ { url = "https://files.pythonhosted.org/packages/63/69/d94db8dac55bcfb6b3243578a3096cfda6c42ea5da292c36919768152ec6/django_split_settings-1.3.2-py3-none-any.whl", hash = "sha256:72bd7dd9f12602585681074d1f859643fb4f6b196b584688fab86bdd73a57dff", size = 6435, upload-time = "2024-07-05T14:29:59.756Z" },
]
[[package]]
name = "djangorestframework"
-version = "3.16.1"
+version = "3.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735 }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442 },
+ { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
+]
+
+[[package]]
+name = "drf-spectacular"
+version = "0.29.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "django" },
+ { name = "djangorestframework" },
+ { name = "inflection" },
+ { name = "jsonschema" },
+ { name = "pyyaml" },
+ { name = "uritemplate" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" },
]
[[package]]
name = "filelock"
version = "3.20.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 }
+sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
+]
+
+[[package]]
+name = "gunicorn"
+version = "26.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
]
[[package]]
name = "hiredis"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048 }
+sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048, upload-time = "2025-10-14T16:33:34.263Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/39/2b789ebadd1548ccb04a2c18fbc123746ad1a7e248b7f3f3cac618ca10a6/hiredis-3.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b7048b4ec0d5dddc8ddd03da603de0c4b43ef2540bf6e4c54f47d23e3480a4fa", size = 82035 },
- { url = "https://files.pythonhosted.org/packages/85/74/4066d9c1093be744158ede277f2a0a4e4cd0fefeaa525c79e2876e9e5c72/hiredis-3.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:e5f86ce5a779319c15567b79e0be806e8e92c18bb2ea9153e136312fafa4b7d6", size = 46219 },
- { url = "https://files.pythonhosted.org/packages/fa/3f/f9e0f6d632f399d95b3635703e1558ffaa2de3aea4cfcbc2d7832606ba43/hiredis-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbdb97a942e66016fff034df48a7a184e2b7dc69f14c4acd20772e156f20d04b", size = 41860 },
- { url = "https://files.pythonhosted.org/packages/4a/c5/b7dde5ec390dabd1cabe7b364a509c66d4e26de783b0b64cf1618f7149fc/hiredis-3.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0fb4bea72fe45ff13e93ddd1352b43ff0749f9866263b5cca759a4c960c776f", size = 170094 },
- { url = "https://files.pythonhosted.org/packages/3e/d6/7f05c08ee74d41613be466935688068e07f7b6c55266784b5ace7b35b766/hiredis-3.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b9baf98050e8f43c2826ab46aaf775090d608217baf7af7882596aef74e7f9", size = 181746 },
- { url = "https://files.pythonhosted.org/packages/0e/d2/aaf9f8edab06fbf5b766e0cae3996324297c0516a91eb2ca3bd1959a0308/hiredis-3.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69079fb0f0ebb61ba63340b9c4bce9388ad016092ca157e5772eb2818209d930", size = 180465 },
- { url = "https://files.pythonhosted.org/packages/8d/1e/93ded8b9b484519b211fc71746a231af98c98928e3ebebb9086ed20bb1ad/hiredis-3.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17f77b79031ea4b0967d30255d2ae6e7df0603ee2426ad3274067f406938236", size = 172419 },
- { url = "https://files.pythonhosted.org/packages/68/13/02880458e02bbfcedcaabb8f7510f9dda1c89d7c1921b1bb28c22bb38cbf/hiredis-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d14f745fc177bc05fc24bdf20e2b515e9a068d3d4cce90a0fb78d04c9c9d9a", size = 166400 },
- { url = "https://files.pythonhosted.org/packages/11/60/896e03267670570f19f61dc65a2137fcb2b06e83ab0911d58eeec9f3cb88/hiredis-3.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ba063fdf1eff6377a0c409609cbe890389aefddfec109c2d20fcc19cfdafe9da", size = 176845 },
- { url = "https://files.pythonhosted.org/packages/f1/90/a1d4bd0cdcf251fda72ac0bd932f547b48ad3420f89bb2ef91bf6a494534/hiredis-3.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1799cc66353ad066bfdd410135c951959da9f16bcb757c845aab2f21fc4ef099", size = 170365 },
- { url = "https://files.pythonhosted.org/packages/f1/9a/7c98f7bb76bdb4a6a6003cf8209721f083e65d2eed2b514f4a5514bda665/hiredis-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cbf71a121996ffac82436b6153290815b746afb010cac19b3290a1644381b07", size = 168022 },
- { url = "https://files.pythonhosted.org/packages/0d/ca/672ee658ffe9525558615d955b554ecd36aa185acd4431ccc9701c655c9b/hiredis-3.3.0-cp313-cp313-win32.whl", hash = "sha256:a7cbbc6026bf03659f0b25e94bbf6e64f6c8c22f7b4bc52fe569d041de274194", size = 20533 },
- { url = "https://files.pythonhosted.org/packages/20/93/511fd94f6a7b6d72a4cf9c2b159bf3d780585a9a1dca52715dd463825299/hiredis-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:a8def89dd19d4e2e4482b7412d453dec4a5898954d9a210d7d05f60576cedef6", size = 22387 },
+ { url = "https://files.pythonhosted.org/packages/6d/39/2b789ebadd1548ccb04a2c18fbc123746ad1a7e248b7f3f3cac618ca10a6/hiredis-3.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b7048b4ec0d5dddc8ddd03da603de0c4b43ef2540bf6e4c54f47d23e3480a4fa", size = 82035, upload-time = "2025-10-14T16:32:23.715Z" },
+ { url = "https://files.pythonhosted.org/packages/85/74/4066d9c1093be744158ede277f2a0a4e4cd0fefeaa525c79e2876e9e5c72/hiredis-3.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:e5f86ce5a779319c15567b79e0be806e8e92c18bb2ea9153e136312fafa4b7d6", size = 46219, upload-time = "2025-10-14T16:32:24.554Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/3f/f9e0f6d632f399d95b3635703e1558ffaa2de3aea4cfcbc2d7832606ba43/hiredis-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbdb97a942e66016fff034df48a7a184e2b7dc69f14c4acd20772e156f20d04b", size = 41860, upload-time = "2025-10-14T16:32:25.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/c5/b7dde5ec390dabd1cabe7b364a509c66d4e26de783b0b64cf1618f7149fc/hiredis-3.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0fb4bea72fe45ff13e93ddd1352b43ff0749f9866263b5cca759a4c960c776f", size = 170094, upload-time = "2025-10-14T16:32:26.148Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d6/7f05c08ee74d41613be466935688068e07f7b6c55266784b5ace7b35b766/hiredis-3.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b9baf98050e8f43c2826ab46aaf775090d608217baf7af7882596aef74e7f9", size = 181746, upload-time = "2025-10-14T16:32:27.844Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d2/aaf9f8edab06fbf5b766e0cae3996324297c0516a91eb2ca3bd1959a0308/hiredis-3.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69079fb0f0ebb61ba63340b9c4bce9388ad016092ca157e5772eb2818209d930", size = 180465, upload-time = "2025-10-14T16:32:29.185Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/1e/93ded8b9b484519b211fc71746a231af98c98928e3ebebb9086ed20bb1ad/hiredis-3.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17f77b79031ea4b0967d30255d2ae6e7df0603ee2426ad3274067f406938236", size = 172419, upload-time = "2025-10-14T16:32:30.059Z" },
+ { url = "https://files.pythonhosted.org/packages/68/13/02880458e02bbfcedcaabb8f7510f9dda1c89d7c1921b1bb28c22bb38cbf/hiredis-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d14f745fc177bc05fc24bdf20e2b515e9a068d3d4cce90a0fb78d04c9c9d9a", size = 166400, upload-time = "2025-10-14T16:32:31.173Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/896e03267670570f19f61dc65a2137fcb2b06e83ab0911d58eeec9f3cb88/hiredis-3.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ba063fdf1eff6377a0c409609cbe890389aefddfec109c2d20fcc19cfdafe9da", size = 176845, upload-time = "2025-10-14T16:32:32.12Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/90/a1d4bd0cdcf251fda72ac0bd932f547b48ad3420f89bb2ef91bf6a494534/hiredis-3.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1799cc66353ad066bfdd410135c951959da9f16bcb757c845aab2f21fc4ef099", size = 170365, upload-time = "2025-10-14T16:32:33.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/9a/7c98f7bb76bdb4a6a6003cf8209721f083e65d2eed2b514f4a5514bda665/hiredis-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cbf71a121996ffac82436b6153290815b746afb010cac19b3290a1644381b07", size = 168022, upload-time = "2025-10-14T16:32:34.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ca/672ee658ffe9525558615d955b554ecd36aa185acd4431ccc9701c655c9b/hiredis-3.3.0-cp313-cp313-win32.whl", hash = "sha256:a7cbbc6026bf03659f0b25e94bbf6e64f6c8c22f7b4bc52fe569d041de274194", size = 20533, upload-time = "2025-10-14T16:32:35.7Z" },
+ { url = "https://files.pythonhosted.org/packages/20/93/511fd94f6a7b6d72a4cf9c2b159bf3d780585a9a1dca52715dd463825299/hiredis-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:a8def89dd19d4e2e4482b7412d453dec4a5898954d9a210d7d05f60576cedef6", size = 22387, upload-time = "2025-10-14T16:32:36.441Z" },
]
[[package]]
name = "identify"
version = "2.6.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311 }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183 },
+ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
+]
+
+[[package]]
+name = "inflection"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
@@ -209,59 +293,59 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 },
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 }
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651 },
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pre-commit"
-version = "4.5.0"
+version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@@ -270,23 +354,73 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428 }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
+]
+
+[[package]]
+name = "psycopg"
+version = "3.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
+]
+
+[package.optional-dependencies]
+binary = [
+ { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
+]
+pool = [
+ { name = "psycopg-pool" },
+]
+
+[[package]]
+name = "psycopg-binary"
+version = "3.3.4"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
+ { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
+ { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
+ { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
+]
+
+[[package]]
+name = "psycopg-pool"
+version = "3.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 },
+ { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
-version = "9.0.1"
+version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -295,21 +429,34 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125 }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668 },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pytest-django"
-version = "4.11.1"
+version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202 }
+sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281 },
+ { url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
+]
+
+[[package]]
+name = "pytest-env"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/69/4db1c30625af0621df8dbe73797b38b6d1b04e15d021dd5d26a6d297f78c/pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3", size = 16163, upload-time = "2026-03-12T22:39:43.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/16/ad52f56b96d851a2bcfdc1e754c3531341885bd7177a128c13ff2ca72ab4/pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3", size = 10400, upload-time = "2026-03-12T22:39:41.887Z" },
]
[[package]]
@@ -319,36 +466,45 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 }
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 },
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
]
[[package]]
name = "redis"
-version = "7.1.0"
+version = "8.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669 }
+sdist = { url = "https://files.pythonhosted.org/packages/53/ae/ed461cca5780b5fc8b9fe8ca0ed98d89508645fb9d880c24cc42c087678f/redis-8.0.0.tar.gz", hash = "sha256:a00c5355432051ac14e593b8b197fc76c887ee12d55a0984f69328a1115fdc49", size = 5101591, upload-time = "2026-05-28T12:45:13.5Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 },
+ { url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" },
]
[package.optional-dependencies]
@@ -356,6 +512,19 @@ hiredis = [
{ name = "hiredis" },
]
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
[[package]]
name = "rich"
version = "14.2.0"
@@ -364,87 +533,141 @@ dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 },
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" },
+ { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" },
+ { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" },
+ { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" },
+ { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" },
]
[[package]]
name = "ruff"
-version = "0.14.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475 },
- { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613 },
- { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458 },
- { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412 },
- { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949 },
- { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470 },
- { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621 },
- { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817 },
- { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549 },
- { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389 },
- { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679 },
- { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677 },
- { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392 },
- { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230 },
- { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397 },
- { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751 },
- { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726 },
- { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215 },
+version = "0.15.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
+ { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
+ { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
+ { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
+ { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
+ { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
+ { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112 }
+sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112, upload-time = "2025-11-28T07:10:18.377Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933 },
+ { url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" },
]
[[package]]
name = "stevedore"
version = "5.6.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074 }
+sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428 },
+ { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" },
]
[[package]]
name = "ty"
-version = "0.0.1a34"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f5/f9/f467d2fbf02a37af5d779eb21c59c7d5c9ce8c48f620d590d361f5220208/ty-0.0.1a34.tar.gz", hash = "sha256:659e409cc3b5c9fb99a453d256402a4e3bd95b1dbcc477b55c039697c807ab79", size = 4735988 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/b7/d5a5c611baaa20e85971a7c9a527aaf3e8fb47e15de88d1db39c64ee3638/ty-0.0.1a34-py3-none-linux_armv6l.whl", hash = "sha256:00c138e28b12a80577ee3e15fc638eb1e35cf5aa75f5967bf2d1893916ce571c", size = 9708675 },
- { url = "https://files.pythonhosted.org/packages/cb/62/0b78976c8da58b90a86d1a1b8816ff4a6e8437f6e52bb6800c4483242e7f/ty-0.0.1a34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbb9c187164675647143ecb56e684d6766f7d5ba7f6874a369fe7c3d380a6c92", size = 9515760 },
- { url = "https://files.pythonhosted.org/packages/39/1f/4e3d286b37aab3428a30b8f5db5533b8ce6e23b1bd84f77a137bd782b418/ty-0.0.1a34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:68b2375b366ee799a896594cde393a1b60414efdfd31399c326bfc136bfc41f3", size = 9064633 },
- { url = "https://files.pythonhosted.org/packages/5d/31/e17049b868f5cac7590c000f31ff9453e4360125416da4e8195e82b5409a/ty-0.0.1a34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f6b68d9673e43bdd5bdcaa6b5db50e873431fc44dde5e25e253e8226ec93ac1", size = 9310295 },
- { url = "https://files.pythonhosted.org/packages/77/1d/7a89b3032e84a01223d0c33e47f33eef436ca36949b28600554a2a4da1f8/ty-0.0.1a34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:832b360fd397c076e294c252db52581b9ecb38d8063d6262ac927610540702be", size = 9498451 },
- { url = "https://files.pythonhosted.org/packages/fa/5e/e782c4367d14b965b1ee9bddc3f3102982ff1cc2dae699c201ecd655e389/ty-0.0.1a34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb6fc497f1feb67e299fd3507ed30498c7e15b31099b3dcdbeca6b7ac2d3129", size = 9912522 },
- { url = "https://files.pythonhosted.org/packages/9c/25/4d72d7174b60adeb9df6e4c5d8552161da2b84ddcebed8ab37d0f7f266ab/ty-0.0.1a34-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:284c8cfd64f255d942ef21953e3d40d087c74dec27e16495bd656decdd208f59", size = 10518743 },
- { url = "https://files.pythonhosted.org/packages/05/c5/30a6e377bcab7d5b65d5c78740635b23ecee647bf268c9dc82a91d41c9ba/ty-0.0.1a34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c34b028305642fd3a9076d4b07d651a819c61a65371ef38cde60f0b54dce6180", size = 10285473 },
- { url = "https://files.pythonhosted.org/packages/97/aa/d2cd564ee37a587c8311383a5687584c9aed241a9e67301ee0280301eef3/ty-0.0.1a34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad997a21648dc64017f11a96b7bb44f088ab0fd589decadc2d686fc97b102f4e", size = 10298873 },
- { url = "https://files.pythonhosted.org/packages/2e/80/c427dabd51b5d8b50fc375e18674c098877a9d6545af810ccff4e40ff74a/ty-0.0.1a34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1afe9798f94c0fbb9e42ff003dfcb4df982f97763d93e5b1d53f9da865a53af", size = 9851399 },
- { url = "https://files.pythonhosted.org/packages/cc/d8/7240c0e13bc3405b190b4437fbc67c86aa70e349b282e5fa79282181532b/ty-0.0.1a34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bd335010aa211fbf8149d3507d6331bdb947d5328ca31388cecdbd2eb49275c3", size = 9261475 },
- { url = "https://files.pythonhosted.org/packages/6b/a1/6538f8fe7a5b1a71b20461d905969b7f62574cf9c8c6af580b765a647289/ty-0.0.1a34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:29ebcc56aabaf6aa85c3baf788e211455ffc9935b807ddc9693954b6990e9a3c", size = 9554878 },
- { url = "https://files.pythonhosted.org/packages/3d/f2/b8ab163b928de329d88a5f04a5c399a40c1c099b827c70e569e539f9a755/ty-0.0.1a34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0cbb5a68fddec83c39db6b5f0a5c5da5a3f7d7620e4bcb4ad5bf3a0c7f89ab45", size = 9651340 },
- { url = "https://files.pythonhosted.org/packages/dc/1b/1e4e24b684ee5f22dda18d86846430b123fb2e985f0c0eb986e6eccec1b9/ty-0.0.1a34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f9b3fd934982a9497237bf39fa472f6d201260ac95b3dc75ba9444d05ec01654", size = 9944488 },
- { url = "https://files.pythonhosted.org/packages/80/b0/6435f1795f76c57598933624af58bf67385c96b8fa3252f5f9087173e21a/ty-0.0.1a34-py3-none-win32.whl", hash = "sha256:bdabc3f1a048bc2891d4184b818a7ee855c681dd011d00ee672a05bfe6451156", size = 9151401 },
- { url = "https://files.pythonhosted.org/packages/73/2e/adce0d7c07f6de30c7f3c125744ec818c7f04b14622a739fe17d4d0bdb93/ty-0.0.1a34-py3-none-win_amd64.whl", hash = "sha256:a4caa2e58685d6801719becbd0504fe61e3ab94f2509e84759f755a0ca480ada", size = 10031079 },
- { url = "https://files.pythonhosted.org/packages/23/0d/1f123c69ce121dcabf5449a456a9a37c3bbad396e9e7484514f1fe568f96/ty-0.0.1a34-py3-none-win_arm64.whl", hash = "sha256:dd02c22b538657b042d154fe2d5e250dfb20c862b32e6036a6ffce2fd1ebca9d", size = 9534879 },
+version = "0.0.49"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/8d/37cb91808069509d43a2a11743e12f1e854fd808dbef2203309d256718cd/ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145", size = 5884753, upload-time = "2026-06-12T03:08:20.221Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/de/9237c6a96356612dd0393db1e94cf21f903616adf3a3701bf3da6e4adc92/ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065", size = 11834671, upload-time = "2026-06-12T03:07:53.062Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/15/daf5a14a5e07012277d450c75325c94614e2acfec4c620c881486118c410/ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471", size = 11589570, upload-time = "2026-06-12T03:08:25.779Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/58/30bdf98436488aca25f0763bf7f92a061528d42461b686453029e845e4c5/ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4", size = 10985236, upload-time = "2026-06-12T03:08:36.664Z" },
+ { url = "https://files.pythonhosted.org/packages/22/45/ece503e4a1396e13a1a9a0cde51afe476a6506a1d557eeadf8ad45c83bc0/ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad", size = 11504302, upload-time = "2026-06-12T03:08:01.664Z" },
+ { url = "https://files.pythonhosted.org/packages/17/dc/5d09333d289dfbca1804eaade125c9e8a1a992a2a592a8b80c5e9b589ca9/ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04", size = 11626629, upload-time = "2026-06-12T03:08:06.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/36/155f41c9dd7237c4b609211f29f77755a139ee6218605dadc7fe21d5e3c8/ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b", size = 12074481, upload-time = "2026-06-12T03:08:09.643Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4c/998ee13cd5045f1f8b36982de7343163832ac53f27debe01b0de0e8bd968/ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d", size = 12678042, upload-time = "2026-06-12T03:08:39.319Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c9/9a505aba85c41ce54cbcaa14f8d79aa084b86151d2d70df11c4655b92898/ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50", size = 12316194, upload-time = "2026-06-12T03:08:18.023Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b8/ded37fb93503294abbc83c36470bb1413bea05048b745881d4470b518a06/ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0", size = 12145507, upload-time = "2026-06-12T03:07:56.505Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/07/392e80d78f02445f695b815bb9eb0fffacda68b03faee38c900f7b990815/ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d", size = 12365967, upload-time = "2026-06-12T03:08:12.553Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d3/31b0c2a7fbedd3373e389cb1d81b8d2128f6f868fafb46557736a6f9aca8/ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b", size = 11475283, upload-time = "2026-06-12T03:08:28.334Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5b/329e101638920b468a3bb63059c9f66ef99b44aac501222c44832a507321/ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d", size = 11645343, upload-time = "2026-06-12T03:08:15.246Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/76/c897e615e32f80ca81c8c1bc49b9a1f72ff9e3cfea0f8345ba505fe28472/ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d", size = 11725585, upload-time = "2026-06-12T03:08:33.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/e1/fdb42ee239f618800842681af5bb8598117e74512c10974a8b7b9086a898/ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731", size = 12237261, upload-time = "2026-06-12T03:08:31.105Z" },
+ { url = "https://files.pythonhosted.org/packages/98/0f/a2d6a5fc9d0786cbeb3c200786da4e18c203589be3984bb5def83ca92320/ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1", size = 11100789, upload-time = "2026-06-12T03:07:59.092Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9d/473ac8bc57b5a2d121da893bf9dd74a118efb19a01d711df1a6e397f05cc/ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1", size = 12204644, upload-time = "2026-06-12T03:08:04.327Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a2/8959249da951ba3977fee20e688d28678b8a1d30a9ed4464228a85d45853/ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f", size = 11558965, upload-time = "2026-06-12T03:08:23.012Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "uritemplate"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
]
[[package]]
@@ -456,7 +679,16 @@ dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799 }
+sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
+]
+
+[[package]]
+name = "whitenoise"
+version = "6.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095 },
+ { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
]