diff --git a/Makefile b/Makefile index ab85d37c..217549eb 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ BACKEND_BASE_DOCKERFILE := projects/backend/Dockerfile.base BACKEND_BASE_HASH := $(shell sha256sum $(BACKEND_BASE_DOCKERFILE) 2>/dev/null | awk '{print $$1}') BACKEND_DEV_SERVICES := auth-service experiment-service telemetry-ingest-service # All services managed by `make dev-*` targets -DEV_ALL_SERVICES := postgres redis auth-service experiment-service telemetry-ingest-service auth-proxy experiment-portal sensor-simulator loki alloy grafana +DEV_ALL_SERVICES := postgres redis auth-service experiment-service telemetry-ingest-service config-service auth-proxy experiment-portal sensor-simulator loki alloy grafana # Default dev credentials for `make dev-seed` DEV_ADMIN_USER ?= admin DEV_ADMIN_EMAIL ?= admin@example.com @@ -679,6 +679,25 @@ script-migrate: script-init: script-create-db script-migrate @echo "✅ Script-service инициализирован" +# Создание базы данных config_db (если не существует) +config-create-db: + @echo "Создание базы данных config_db..." + @$(DOCKER_COMPOSE) exec -T postgres psql -U postgres -d postgres -c "SELECT 1 FROM pg_database WHERE datname = 'config_db'" | grep -q 1 && \ + echo "✅ База данных config_db уже существует" || \ + ($(DOCKER_COMPOSE) exec -T postgres psql -U postgres -d postgres -c "CREATE DATABASE config_db;" && \ + echo "✅ База данных config_db создана") + +# Применение миграций config-service +config-migrate: + @echo "Применение миграций config-service..." + @$(DOCKER_COMPOSE) exec -T config-service python -m bin.migrate --database-url "$${CONFIG_DATABASE_URL:-postgresql://config_user:config_password@postgres:5432/config_db}" || \ + $(DOCKER_COMPOSE) exec config-service python -m bin.migrate --database-url "$${CONFIG_DATABASE_URL:-postgresql://config_user:config_password@postgres:5432/config_db}" + @echo "✅ Миграции config-service применены" + +# Инициализация config-service (создание БД + миграции) +config-init: config-create-db config-migrate + @echo "✅ Config-service инициализирован" + # ============================================ # Production Deploy (Yandex Cloud) # ============================================ diff --git a/docker-compose.yml b/docker-compose.yml index b97479d3..c627be79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,8 @@ services: EXPERIMENT_DB_PASSWORD: ${EXPERIMENT_DB_PASSWORD:-experiment_password} SCRIPT_DB_USER: ${SCRIPT_DB_USER:-script_user} SCRIPT_DB_PASSWORD: ${SCRIPT_DB_PASSWORD:-script_password} + CONFIG_DB_USER: ${CONFIG_DB_USER:-config_user} + CONFIG_DB_PASSWORD: ${CONFIG_DB_PASSWORD:-config_password} ports: - "${POSTGRES_PORT:-5433}:5432" volumes: @@ -135,6 +137,22 @@ services: timeout: 5s retries: 5 + # Config DB migrations — запускается один раз перед config-service + config-migrate: + build: + context: ./projects/backend + dockerfile: services/config-service/Dockerfile + container_name: config-migrate + environment: + - DATABASE_URL=${CONFIG_DATABASE_URL:-postgresql://config_user:config_password@postgres:5432/config_db} + command: ["python", "-m", "bin.migrate"] + depends_on: + postgres: + condition: service_healthy + networks: + - experiment-tracking-network + restart: "no" + # Auth DB migrations — запускается один раз перед auth-service auth-migrate: build: @@ -318,6 +336,40 @@ services: retries: 3 start_period: 40s + # Config Service - runtime configuration (feature flags, QoS, kill-switches) + config-service: + build: + context: ./projects/backend + dockerfile: services/config-service/Dockerfile + container_name: config-service + env_file: + - .env + environment: + - DATABASE_URL=${CONFIG_DATABASE_URL:-postgresql://config_user:config_password@postgres:5432/config_db} + - ENV=${CONFIG_SERVICE_ENV:-development} + - HOST=${CONFIG_SERVICE_HOST:-0.0.0.0} + - PORT=${CONFIG_SERVICE_PORT:-8005} + - DB_POOL_SIZE=${CONFIG_SERVICE_DB_POOL_SIZE:-10} + - AUTH_SERVICE_URL=${AUTH_SERVICE_URL:-http://auth-service:8001/api/v1} + - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:8080} + command: ["python", "-m", "config_service.main"] + ports: + - "${CONFIG_SERVICE_PORT:-8005}:8005" + depends_on: + config-migrate: + condition: service_completed_successfully + postgres: + condition: service_healthy + networks: + - experiment-tracking-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8005/health').read()\" || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + # Auth Proxy - BFF для фронтенда auth-proxy: build: diff --git a/docker-entrypoint-initdb.d/init-multiple-databases.sh b/docker-entrypoint-initdb.d/init-multiple-databases.sh index 0f86ebe9..631b0e9f 100755 --- a/docker-entrypoint-initdb.d/init-multiple-databases.sh +++ b/docker-entrypoint-initdb.d/init-multiple-databases.sh @@ -8,6 +8,8 @@ EXPERIMENT_DB_USER=${EXPERIMENT_DB_USER:-experiment_user} EXPERIMENT_DB_PASSWORD=${EXPERIMENT_DB_PASSWORD:-experiment_password} SCRIPT_DB_USER=${SCRIPT_DB_USER:-script_user} SCRIPT_DB_PASSWORD=${SCRIPT_DB_PASSWORD:-script_password} +CONFIG_DB_USER=${CONFIG_DB_USER:-config_user} +CONFIG_DB_PASSWORD=${CONFIG_DB_PASSWORD:-config_password} echo "Creating databases and users..." @@ -105,5 +107,34 @@ psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "script_db" <<-EOSQ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $SCRIPT_DB_USER; EOSQL +# Create config_db and config_user +echo "Creating database: config_db" +psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -c "CREATE DATABASE config_db;" 2>&1 | grep -v "already exists" || true + +echo "Creating user: $CONFIG_DB_USER" +psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$CONFIG_DB_USER') THEN + CREATE USER $CONFIG_DB_USER WITH PASSWORD '$CONFIG_DB_PASSWORD'; + ELSE + ALTER USER $CONFIG_DB_USER WITH PASSWORD '$CONFIG_DB_PASSWORD'; + END IF; + END + \$\$; +EOSQL + +echo "Granting privileges on config_db to $CONFIG_DB_USER" +psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + GRANT ALL PRIVILEGES ON DATABASE config_db TO $CONFIG_DB_USER; +EOSQL + +psql -v ON_ERROR_STOP=0 --username "$POSTGRES_USER" --dbname "config_db" <<-EOSQL + CREATE EXTENSION IF NOT EXISTS pgcrypto; + GRANT ALL ON SCHEMA public TO $CONFIG_DB_USER; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $CONFIG_DB_USER; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $CONFIG_DB_USER; +EOSQL + echo "✅ Multiple databases and users initialized" diff --git a/docs/RFC/fc-0001-config-service.md b/docs/RFC/fc-0001-config-service.md deleted file mode 100644 index ed659a19..00000000 --- a/docs/RFC/fc-0001-config-service.md +++ /dev/null @@ -1,449 +0,0 @@ -# RFC-0012: config-service - -**Статус:** Draft - -**Автор(ы):** Ivan Khokhlov - -**Дата:** 17-04-2026 - -**Тип:** New Service - ---- - -## Резюме - -Иногда нужно управлять поведением системы на лету, поднять таймауты походв в другие сервисы, базы данных, включение экспериментальной функциональности, добавление новых типов экспериментов, включение деградации при проблемах в сервисах. Сейчас этим нельзя управлять налету. - ---- - -## Мотивация - -Почему нужен новый сервис? Какие проблемы в текущей архитектуре он решает? - -- **Проблема 1: Отсутствие runtime конфигурации** - Сейчас для изменения параметров поведения системы (таймауты, feature flags, пороги деградации) требуется изменить конфигурационные файлы и перезапустить сервис, что приводит к простою и невозможности оперативного реагирования на проблемы в production. - -- **Проблема 2: Декентрализованное управление конфигурацией** - Конфигурация разбросана по разным сервисам в переменных окружения, ConfigMap'ах Kubernetes или локальных файлах, что усложняет аудит, версионирование и обеспечение консистентности. - -- **Ограничения текущего подхода** - Нельзя оперативно реагировать на инциденты (например, увеличить таймаут к проблемному внешнему API или отключить проблемную функциональность без кодовых изменений и деплоя). - ---- - -## Дизайн сервиса - -### Основные компоненты - -| Компонент | Описание | Технология | -|-----------|---------|-----------| -| API | REST эндпоинты (JSON over HTTP) | Python aiohttp | -| Хранилище конфигурации | Таблицы для разных типов конфигурации | PostgreSQL | -| Обработчик событий | Реакция на изменения конфигурации и распространение | Polling | -| Worker фоновой обработки | Асинхронные задачи (валидация, агрегация) | Р worker на основе asyncio | - -### API - -Основные эндпоинты для управления конфигурацией: - -``` -POST /api/v1/config # Создание новой конфигурации -GET /api/v1/config # Получение списка всех конфигураций (без фильтрации по сервису/проекту) -GET /api/v1/config/{config_id} # Получение конкретной конфигурации по ID -PATCH /api/v1/config/{config_id} # Частичное обновление конфигурации -DELETE /api/v1/config/{config_id} # Удаление конфигурации (soft delete) -POST /api/v1/config/{config_id}/activate # Активация конфигурации -POST /api/v1/config/{config_id}/deactivate # Деактивация конфигурации -GET /api/v1/config/{config_id}/history # История изменений конфигурации -``` - -#### Внутренний эндпоинт для сервисов-потребителей - -Сервисы платформы используют внутренний эндпоинт для получения набора конфигураций, актуальных для конкретного сервиса и проекта, с поддержкой кэширования через ETag/Last-Modified: - -``` -GET /api/v1/configs/bulk?service={service_name}&project={project_id} # Получение всех конфигураций для сервиса и проекта -``` - -**Параметры запроса:** -- `service` (обязательно): идентификатор сервиса (например, `experiment-service`, `telemetry-ingest`) -- `project` (опционально): идентификатор проекта, если конфигурации scoped по проектам - -**Ответ:** -- JSON-объект вида `{ "configs": { "": , ... } }` где ключ — это имя конфигурации (например, `timeout_ms`, `feature_flag_enabled`), а значение — соответствующее значение из поля `value` конфигурации. -- Заголовок `ETag` — хеш всего набора конфигураций. -- Заголовок `Last-Modified` — timestamp самого свежего обновления среди возвращаемых конфигураций. - -**Условные запросы:** -Клиент может передавать заголовок `If-None-Match` со значением предыдущего ETag. Если набор конфигураций не изменился, сервис возвращает **304 Not Modified** и пустое тело. - -**Пример запроса/ответа:** - -```http -GET /api/v1/configs/bulk?service=experiment-service&project=project-a -Accept: application/json -If-None-Match: "W/\"abc123\"" -``` - -*Если конфигурации изменились:* -```http -HTTP/1.1 200 OK -Content-Type: application/json -ETag: "W/\"def456\"" -Last-Modified: Thu, 17 Apr 2026 10:30:00 GMT - -{ - "configs": { - "timeout_ms": 5000, - "max_retries": 3, - "feature_new_ui_enabled": true - } -} -``` - -*Если конфигурации не изменились:* -```http -HTTP/1.1 304 Not Modified -ETag: "W/\"abc123\"" -Last-Modified: Thu, 17 Apr 2026 10:15:00 GMT -``` - -**Типы конфигурации (поле `config_type`):** -- `feature_flag` - булевые переключатели функциональности -- `timeout` - настройки таймаутов для внешних вызовов -- `rate_limit` - ограничения частоты запросов -- `circuit_breaker` - настройки срабатывания и восстановления -- `experiment_param` - параметры A/B тестов и экспериментов -- `degradation_rule` - правила перехода в режим деградации - -**Пример запроса/ответа для feature flag:** - -```json -// POST /api/v1/config -{ - "key": "new_payment_gateway_enabled", - "config_type": "feature_flag", - "description": "Включить новый шлюз платежей для эксперимента", - "value": { - "enabled": true, - "rollout_percentage": 15 - }, - "conditions": { - "user_segment": "premium", - "geo": ["US", "EU"] - }, - "metadata": { - "experiment_id": "exp_payment_v2", - "start_date": "2026-04-18", - "end_date": "2026-05-18" - } -} - -// Response 201 -{ - "id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", - "key": "new_payment_gateway_enabled", - "config_type": "feature_flag", - "description": "Включить новый шлюз платежей для эксперимента", - "value": { - "enabled": true, - "rollout_percentage": 15 - }, - "conditions": { - "user_segment": "premium", - "geo": ["US", "EU"] - }, - "metadata": { - "experiment_id": "exp_payment_v2", - "start_date": "2026-04-18", - "end_date": "2026-05-18" - }, - "is_active": true, - "version": 1, - "created_at": "2026-04-17T10:00:00Z", - "updated_at": "2026-04-17T10:00:00Z", - "created_by": "ivan.khokhlov@company.com" -} -``` - -### Интеграция с другими сервисами - -**Какие сервисы вызывают новый сервис:** -- **auth-service** - проверяет доступ пользователей к изменению конфигурации (RBAC) -- **Все сервисы-platform** (experiment-service, telemetry-ingest, и т.д.) - периодически опрашивают внутренний эндпоинт `/api/v1/configs/bulk` с параметрами `service` и `project`, кэшируют ответ in-process и используют условные запросы (If-None-Match) для получения 304 при отсутствии изменений -- **Системы мониторинга и алертинга** - могут менять конфигурацию degradation правил при обнаружении проблем (через обычные API эндпоинты) - -**На какие внешние сервисы зависит:** -- **PostgreSQL** - перманентное хранилище всех конфигураций и их истории -- **auth-service** - проверка прав доступа при изменении конфигурации (через HTTP) - ---- - -## Реализация - -### Структура проекта - -``` -projects/backend/services/config-service/ -├── src/config_service/ -│ ├── api/ # REST routes (v1) -│ │ ├── v1/ -│ │ │ ├── routes/ -│ │ │ │ ├── config.py -│ │ │ │ └── health.py -│ │ │ └── dependencies.py -│ │ └── __init__.py -│ ├── services/ # Бизнес-логика -│ │ ├── config_service.py -│ │ ├── cache_service.py -│ │ ├── event_service.py -│ │ └── validation_service.py -│ ├── repositories/ # Слой доступа к данным -│ │ ├── config_repository.py -│ │ └── history_repository.py -│ ├── models/ # Pydantic модели и SQLAlchemy модели -│ │ ├── config.py -│ │ ├── history.py -│ │ └── enums.py -│ ├── workers/ # Фоновые обработчики -│ │ └── config_worker.py -│ ├── utils/ # Вспомогательные функции -│ │ └── conditions.py -│ └── main.py # Entry point -├── tests/ -│ ├── unit/ -│ │ ├── test_config_service.py -│ │ ├── test_cache_service.py -│ │ └── test_validation.py -│ ├── integration/ -│ │ └── test_config_api.py -│ └── conftest.py -├── migrations/ # SQL миграции (Alembic) -│ ├── versions/ -│ └── env.py -├── openapi/ # OpenAPI 3.1 спецификация -│ └── config-service.yaml -├── pyproject.toml -├── Dockerfile -└── README.md -``` - -### Порт и переменные окружения - -- **Порт:** 8004 (следовать номерации сервисов: auth-8001, experiment-8002, telemetry-ingest-8003) -- **Переменные:** - - `DATABASE_URL`: PostgreSQL connection string (обязательно) - - `AUTH_SERVICE_URL`: auth-service endpoint для проверки прав (обязательно) - - `CONFIG_CACHE_TTL_SECONDS`: TTL для кеша конфигурации в Redis (по умолчанию: 5) - - `CONFIG_POLL_INTERVAL_SECONDS`: интервал опроса изменений для worker'ов (по умолчанию: 1) - - `LOG_LEVEL`: уровень логирования (INFO/DEBUG/WARNING/ERROR) - -### Миграции БД - -Начальная схема для хранения конфигураций, scoped по сервису и проекту: - -```sql --- Основная таблица конфигураций -CREATE TABLE configs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - service_name VARCHAR(100) NOT NULL, -- например, 'experiment-service', 'telemetry-ingest' - project_id VARCHAR(100), -- nullable, если конфигурация не привязана к конкретному проекту - key VARCHAR(255) NOT NULL, -- имя конфигурации внутри сервиса/проекта - config_type VARCHAR(50) NOT NULL, -- feature_flag, timeout, rate_limit, etc. - description TEXT, - value JSONB NOT NULL, -- Собственное значение конфигурации - conditions JSONB, -- Условия применения (segments, geo, etc.) - metadata JSONB, -- Дополнительные данные (эксперимент, даты и т.д.) - is_active BOOLEAN DEFAULT FALSE, - version INTEGER DEFAULT 1, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR(255), -- email или ID пользователя - updated_by VARCHAR(255), - UNIQUE (service_name, project_id, key) -- позволяем одно и то же имя конфигурации для разных сервисов/проектов -); - --- История изменений конфигураций (для аудита и отката) -CREATE TABLE config_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - config_id UUID REFERENCES configs(id) ON DELETE CASCADE, - service_name VARCHAR(100) NOT NULL, - project_id VARCHAR(100), - key VARCHAR(255) NOT NULL, - config_type VARCHAR(50) NOT NULL, - description TEXT, - value JSONB NOT NULL, - conditions JSONB, - metadata JSONB, - is_active BOOLEAN, - version INTEGER, - changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - changed_by VARCHAR(255), - change_reason TEXT -- Почему было сделано изменение -); - --- Индексы для быстрого поиска -CREATE INDEX idx_configs_service ON configs(service_name); -CREATE INDEX idx_configs_project ON configs(project_id); -CREATE INDEX idx_configs_key ON configs(key); -CREATE INDEX idx_configs_type ON configs(config_type); -CREATE INDEX idx_configs_active ON configs(is_active) WHERE is_active = TRUE; -CREATE INDEX idx_config_history_config_id ON config_history(config_id); -CREATE INDEX idx_config_history_changed_at ON config_history(changed_at); - --- Триггер для автоматического обновления updated_at -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ language 'plpgsql'; - -CREATE TRIGGER update_configs_updated_at -BEFORE UPDATE ON configs -FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); - --- Триггер для сохранения истории при изменении конфигурации -CREATE OR REPLACE FUNCTION log_config_change() -RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO config_history ( - config_id, service_name, project_id, key, config_type, description, value, conditions, - metadata, is_active, version, changed_by, change_reason - ) VALUES ( - OLD.id, OLD.service_name, OLD.project_id, OLD.key, OLD.config_type, OLD.description, OLD.value, - OLD.conditions, OLD.metadata, OLD.is_active, OLD.version, - COALESCE(NEW.updated_by, OLD.updated_by, 'system'), - NEW.change_reason - ); - RETURN NEW; -END; -$$ language 'plpgsql'; - -CREATE TRIGGER log_config_update -AFTER UPDATE ON configs -FOR EACH ROW EXECUTE FUNCTION log_config_change(); -``` - ---- - -## Тестирование - -### Unit tests - -- Тестирование бизнес-логики сервисов (config_service, cache_service, validation_service) -- Тестирование репозиториев (запросы к PostgreSQL с использованием моков или in-memory SQLite) -- Мокирование внешних зависимостей (auth-service для проверки прав, Redis и RabbitMQ) -- Тестирование Pydantic моделей и валидации входных данных -- Тестирование условий применения конфигурации (segments, geo, время и т.д.) - -### Integration tests - -- E2E сценарии через API: создание конфигурации → чтение → обновление → деактивация → удаление -- Проверка интеграции с auth-service: эндпоинты защищены и требуют соответствующих ролей -- Тестирование обработки одновременных обновлений конфигурации (гонки условий) - -### Load tests (опционально, если критично для высоконагруженных систем) - -Нагрузка на сервис будет небольшая, несколько десятков RPS. По этому пока не вижу смысла в нагрузочном тестировании - ---- - -## Развёртывание - -### Docker Compose (dev) - -```yaml -config-service: - image: config-service:latest - ports: - - "8004:8004" - environment: - DATABASE_URL: postgresql://user:pass@postgres:5432/config_service_db - AUTH_SERVICE_URL: http://auth-service:8001 - CONFIG_CACHE_TTL_SECONDS: 5 - CONFIG_POLL_INTERVAL_SECONDS: 1 - LOG_LEVEL: INFO - depends_on: - - postgres -``` - -### Миграции в CI/CD - -```bash -# При деплое на stage/prod -make config-service-migrate -``` - -### Мониторинг - -- OpenTelemetry экспорт (OTLP) для трассировки запросов и операций -- Логирование в Loki (через Alloy) с уровнем INFO в продакшене -- Метрики в Prometheus: количество запросов, задержки, ошибки, размер кеша -- Графики/алерты в Grafama: - - Задержки API эндпоинтов (p95 < 100ms) - - Количество ошибок валидации и авторизации - ---- - -## Альтернативы - -Почему выбран этот подход вместо других? - -- **Альтернатива 1: Использование только переменных окружения и ConfigMaps** - Описание: Хранить всю конфигурацию в переменных окружения или Kubernetes ConfigMaps. Плюсы: простота реализации, нет внешних зависимостей. Минусы: требует перезапуска сервисов при изменении, сложно управлять условиями и версионированием, нет истории изменений. - -- **Альтернатива 2: Сторонние решения (Consul, etcd, Apache Zookeeper)** - Описание: Использовать специализированные системы для distributed конфигурации. Плюсы: готовые решения, поддержка наблюдения за изменениями, сильная консистентность. Минусы: дополнительная сложность в инфраструктуре, требуется изучение новой технологии, потенциальный vendor lock-in. - -- **Выбранный подход:** собственный сервис на основе PostgreSQL + Redis лучше, потому что: - 1. Использует уже существующие технологии в стеке (PostgreSQL, Redis, RabbitMQ) - 2. Позволяет гибко определять схему конфигурации под наши нужды (типы, условия, метаданные) - 3. Обеспечивает полный контроль над производительностью и оптимизацией - 4. Легко интегрируется с существующими системами мониторинга и алертинга - 5. Позволяет реализовать сложную логику применения конфигурации (условия, сегментация, время) - 6. Обеспечивает историю изменений и аудит из коробки - ---- - -## Известные ограничения / Tech Debt - -- **Ограничение 3: Ограничение размера значения** - Поле `value` типа JSONB имеет практический лимит размера (~1GB), но extremely большие конфигурации могут влиять на производительность. Следует документировать рекомендуемый максимальный размер. - ---- - -## План реализации - -### Phase 1: MVP (неделя 1) - -- [ ] Структура проекта + Dockerfile -- [ ] API базовые эндпоинты (CRUD для конфигураций) -- [ ] БД миграции (таблицы configs и config_history) -- [ ] Unit tests (сервисы, репозитории, валидация) -- [ ] Интеграция с Redis для кеширования - -### Phase 2: Интеграция (неделя 2) - -- [ ] Интеграция с auth-service (проверка прав через middleware) -- [ ] Integration tests (полные сценарии через API) -- [ ] OpenAPI 3.1 спецификация -- [ ] Health check эндпоинт - -### Phase 3: Production (неделя 3) - -- [ ] OTLP/Loki конфигурация (трассировка и логирование) -- [ ] Мониторинг/алерты (задержки, ошибки, hit rate кеша) -- [ ] Documentation (руководство по использованию для сервисов-потребителей) -- [ ] Security audit (проверка авторизации и валидации входных данных) - ---- - -## Ссылки - -- Related ADR: [docs/adr/XXXX-название.md](../adr/) -- Issue/Epic: [GitHub issue #XXX](https://github.com/) -- Slack thread: (если есть обсуждение) - ---- - -## История изменений - -| Дата | Автор | Изменение | -|------|-------|----------| -| 2026-04-17 | [Ivan Khokhlov] | Первая версия | diff --git a/docs/RFC/rfc-0001-config-service.md b/docs/RFC/rfc-0001-config-service.md index 10e093e4..f538296d 100644 --- a/docs/RFC/rfc-0001-config-service.md +++ b/docs/RFC/rfc-0001-config-service.md @@ -1,6 +1,6 @@ # RFC-0001: config-service -**Статус:** Review +**Статус:** Approved **Автор(ы):** Ivan Khokhlov diff --git a/env.docker.example b/env.docker.example index 25c33384..e23861e4 100644 --- a/env.docker.example +++ b/env.docker.example @@ -23,17 +23,22 @@ AUTH_DB_USER=auth_user AUTH_DB_PASSWORD=auth_password EXPERIMENT_DB_USER=experiment_user EXPERIMENT_DB_PASSWORD=experiment_password +CONFIG_DB_USER=config_user +CONFIG_DB_PASSWORD=config_password # URL для подключения из контейнеров (использует имя сервиса) # Auth Service использует AUTH_DATABASE_URL AUTH_DATABASE_URL=postgresql://auth_user:auth_password@postgres:5432/auth_db # Experiment Service использует EXPERIMENT_DATABASE_URL EXPERIMENT_DATABASE_URL=postgresql://experiment_user:experiment_password@postgres:5432/experiment_db +# Config Service использует CONFIG_DATABASE_URL +CONFIG_DATABASE_URL=postgresql://config_user:config_password@postgres:5432/config_db # URL для подключения с хоста (для локальной разработки) # Используется порт 5433, чтобы не конфликтовать с локальным PostgreSQL на 5432 AUTH_DATABASE_URL_LOCAL=postgresql://auth_user:auth_password@localhost:5433/auth_db EXPERIMENT_DATABASE_URL_LOCAL=postgresql://experiment_user:experiment_password@localhost:5433/experiment_db +CONFIG_DATABASE_URL_LOCAL=postgresql://config_user:config_password@localhost:5433/config_db # Legacy DATABASE_URL (для обратной совместимости, использует experiment_db) DATABASE_URL=postgresql://experiment_user:experiment_password@postgres:5432/experiment_db @@ -79,6 +84,14 @@ TELEMETRY_BROKER_URL_LOCAL=redis://localhost:6379/0 OTEL_EXPORTER_ENDPOINT=http://otel-collector:4318 OTEL_EXPORTER_ENDPOINT_LOCAL=http://localhost:4318 +# ============================================ +# Config Service +# ============================================ +CONFIG_SERVICE_ENV=development +CONFIG_SERVICE_HOST=0.0.0.0 +CONFIG_SERVICE_PORT=8005 +CONFIG_SERVICE_DB_POOL_SIZE=10 + # ============================================ # Auth Proxy (BFF) # ============================================ diff --git a/projects/backend/services/config-service/Dockerfile b/projects/backend/services/config-service/Dockerfile new file mode 100644 index 00000000..2e75bff9 --- /dev/null +++ b/projects/backend/services/config-service/Dockerfile @@ -0,0 +1,30 @@ +ARG BACKEND_BASE_IMAGE=backend-base:local +FROM ${BACKEND_BASE_IMAGE} AS production + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Copy backend_common directory first (needed for backend-common dependency) +COPY common ./common + +WORKDIR /app/services/config-service + +# Copy dependency files (for better layer caching) +COPY services/config-service/pyproject.toml services/config-service/poetry.lock services/config-service/README.md ./ + +# Install dependencies +RUN poetry install --no-root + +# Copy source code +COPY services/config-service/src ./src +COPY services/config-service/bin ./bin +COPY services/config-service/migrations ./migrations + +# Set PYTHONPATH to include src directory and backend-common +ENV PYTHONPATH=/app/services/config-service/src:/app/common/src + +EXPOSE 8004 + +CMD ["python", "-m", "config_service.main"] diff --git a/projects/backend/services/config-service/README.md b/projects/backend/services/config-service/README.md new file mode 100644 index 00000000..6aa613af --- /dev/null +++ b/projects/backend/services/config-service/README.md @@ -0,0 +1,28 @@ +# config-service + +Runtime configuration service for the platform. Provides feature flags, QoS parameters, and kill-switches. + +Port: **8004** + +## Quick start + +```bash +make config-init # create config_db + apply migrations +make dev-up # start the full stack +curl http://localhost:8004/health +``` + +## API + +- `POST /api/v1/config` — create config +- `GET /api/v1/config` — list configs +- `GET /api/v1/config/{id}` — get config +- `PATCH /api/v1/config/{id}` — update config (requires `If-Match`) +- `DELETE /api/v1/config/{id}` — soft delete +- `POST /api/v1/config/{id}/activate` — activate +- `POST /api/v1/config/{id}/deactivate` — deactivate +- `POST /api/v1/config/{id}/rollback` — rollback to previous version +- `GET /api/v1/config/{id}/history` — change history +- `GET /api/v1/configs/bulk` — bulk fetch for SDK polling (ETag/304 support) +- `GET /api/v1/schemas` — list active JSON schemas +- `PUT /api/v1/schemas/{type}` — update schema (additive changes only) diff --git a/projects/backend/services/config-service/bin/migrate.py b/projects/backend/services/config-service/bin/migrate.py new file mode 100644 index 00000000..d9da4fa1 --- /dev/null +++ b/projects/backend/services/config-service/bin/migrate.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Minimal SQL migration runner for Config Service.""" +# pyright: reportMissingImports=false +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import os +from pathlib import Path +from typing import Dict + +import asyncpg + + +def _default_migrations_dir() -> Path: + return Path(__file__).resolve().parent.parent / "migrations" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Apply SQL migrations sequentially.") + parser.add_argument( + "--database-url", + "-d", + default=os.getenv("DATABASE_URL"), + help="PostgreSQL connection string. Defaults to DATABASE_URL env variable.", + ) + parser.add_argument( + "--migrations-dir", + "-m", + type=Path, + default=_default_migrations_dir(), + help="Directory with *.sql migrations (sorted lexicographically).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Only list pending migrations without applying.", + ) + return parser.parse_args() + + +async def ensure_schema_table(conn: asyncpg.Connection) -> None: + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS schema_migrations ( + version text PRIMARY KEY, + checksum text NOT NULL, + applied_at timestamptz NOT NULL DEFAULT now() + ); + """ + ) + + +def load_migrations(directory: Path) -> Dict[str, Path]: + if not directory.exists(): + raise FileNotFoundError(f"Migrations directory does not exist: {directory}") + migrations: Dict[str, Path] = {} + for path in sorted(directory.glob("*.sql")): + version = path.stem + if version in migrations: + raise ValueError(f"Duplicate migration version detected: {version}") + migrations[version] = path + if not migrations: + raise ValueError(f"No *.sql files found in {directory}") + return migrations + + +async def apply_migrations(database_url: str, migrations_dir: Path, dry_run: bool) -> None: + migrations = load_migrations(migrations_dir) + conn = await asyncpg.connect(database_url) + try: + await ensure_schema_table(conn) + rows = await conn.fetch("SELECT version, checksum FROM schema_migrations") + applied = {row["version"]: row["checksum"] for row in rows} + pending = [] + for version, path in migrations.items(): + sql = path.read_text(encoding="utf-8") + checksum = hashlib.sha256(sql.encode("utf-8")).hexdigest() + if version in applied: + if applied[version] != checksum: + raise RuntimeError( + f"Checksum mismatch for {version}: " + f"{applied[version]} (db) != {checksum} (file)" + ) + continue + pending.append((version, path, sql, checksum)) + + if not pending: + print("No pending migrations.") + return + + for version, path, sql, checksum in pending: + if dry_run: + print(f"[dry-run] Pending migration: {path.name}") + continue + print(f"Applying {path.name}...") + async with conn.transaction(): + await conn.execute(sql) + await conn.execute( + "INSERT INTO schema_migrations (version, checksum) VALUES ($1, $2)", + version, + checksum, + ) + if dry_run: + print(f"{len(pending)} migration(s) pending.") + else: + print(f"Applied {len(pending)} migration(s).") + finally: + await conn.close() + + +async def main_async() -> None: + args = parse_args() + if not args.database_url: + raise SystemExit("Database URL must be provided via --database-url or DATABASE_URL env.") + await apply_migrations(args.database_url, args.migrations_dir, args.dry_run) + + +def main() -> None: + asyncio.run(main_async()) + + +if __name__ == "__main__": + main() diff --git a/projects/backend/services/config-service/env.example b/projects/backend/services/config-service/env.example new file mode 100644 index 00000000..9baec827 --- /dev/null +++ b/projects/backend/services/config-service/env.example @@ -0,0 +1,15 @@ +ENV=development +HOST=0.0.0.0 +PORT=8005 + +DATABASE_URL=postgresql://config_user:config_password@localhost:5433/config_db +DB_POOL_SIZE=10 + +AUTH_SERVICE_URL=http://localhost:8001/api/v1 + +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 + +# OTEL_EXPORTER_ENDPOINT=http://localhost:4318 + +IDEMPOTENCY_TTL_MINUTES=15 +WORKER_INTERVAL_SECONDS=60 diff --git a/projects/backend/services/config-service/migrations/001_initial_schema.sql b/projects/backend/services/config-service/migrations/001_initial_schema.sql new file mode 100644 index 00000000..675e8693 --- /dev/null +++ b/projects/backend/services/config-service/migrations/001_initial_schema.sql @@ -0,0 +1,110 @@ +BEGIN; + +-- Configs table +CREATE TABLE IF NOT EXISTS configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + service_name VARCHAR(128) NOT NULL, + project_id VARCHAR(128), + key VARCHAR(128) NOT NULL, + config_type VARCHAR(32) NOT NULL, + description TEXT, + value JSONB NOT NULL DEFAULT '{}', + metadata JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT true, + is_critical BOOLEAN NOT NULL DEFAULT false, + is_sensitive BOOLEAN NOT NULL DEFAULT false, + version INTEGER NOT NULL DEFAULT 1, + created_by VARCHAR(255) NOT NULL, + updated_by VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Unique constraint: only one active (non-deleted) record per (service, project, key) +CREATE UNIQUE INDEX IF NOT EXISTS idx_configs_unique_active + ON configs (service_name, COALESCE(project_id, ''), key) + WHERE deleted_at IS NULL; + +-- Fast lookup of active configs for bulk endpoint +CREATE INDEX IF NOT EXISTS idx_configs_active + ON configs (service_name, project_id, is_active) + WHERE is_active = true AND deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_configs_service + ON configs (service_name) + WHERE deleted_at IS NULL; + +-- Auto-update updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS configs_updated_at ON configs; +CREATE TRIGGER configs_updated_at + BEFORE UPDATE ON configs + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Config history table +CREATE TABLE IF NOT EXISTS config_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_id UUID NOT NULL REFERENCES configs(id), + version INTEGER NOT NULL, + service_name VARCHAR(128) NOT NULL, + key VARCHAR(128) NOT NULL, + config_type VARCHAR(32) NOT NULL, + value JSONB NOT NULL DEFAULT '{}', + metadata JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL, + changed_by VARCHAR(255) NOT NULL, + change_reason TEXT, + source_ip VARCHAR(45), + user_agent TEXT, + correlation_id VARCHAR(128), + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_config_history_config_id + ON config_history (config_id, version DESC); + +-- Config schemas table +CREATE TABLE IF NOT EXISTS config_schemas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_type VARCHAR(32) NOT NULL, + schema JSONB NOT NULL, + version INTEGER NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + created_by VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Only one active schema per config_type at a time +CREATE UNIQUE INDEX IF NOT EXISTS idx_config_schemas_active_unique + ON config_schemas (config_type) + WHERE is_active = true; + +CREATE INDEX IF NOT EXISTS idx_config_schemas_type + ON config_schemas (config_type, version DESC); + +-- Idempotency keys table +CREATE TABLE IF NOT EXISTS idempotency_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + idempotency_key VARCHAR(255) NOT NULL UNIQUE, + user_id VARCHAR(255) NOT NULL, + request_path TEXT NOT NULL, + request_hash VARCHAR(64) NOT NULL, + response_status INTEGER NOT NULL, + response_body JSONB NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_idempotency_keys_expires_at + ON idempotency_keys (expires_at); + +COMMIT; diff --git a/projects/backend/services/config-service/migrations/002_seed_schemas.sql b/projects/backend/services/config-service/migrations/002_seed_schemas.sql new file mode 100644 index 00000000..edb51fab --- /dev/null +++ b/projects/backend/services/config-service/migrations/002_seed_schemas.sql @@ -0,0 +1,50 @@ +BEGIN; + +INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) +VALUES ( + 'feature_flag', + '{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" } + }, + "additionalProperties": false + }'::jsonb, + 1, + true, + 'system' +) +ON CONFLICT DO NOTHING; + +INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) +VALUES ( + 'qos', + '{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["__default__"], + "additionalProperties": { "$ref": "#/$defs/qosSettings" }, + "properties": { + "__default__": { "$ref": "#/$defs/qosSettings" } + }, + "$defs": { + "qosSettings": { + "type": "object", + "required": ["timeout_ms", "retries"], + "properties": { + "timeout_ms": { "type": "integer", "minimum": 1, "maximum": 600000 }, + "retries": { "type": "integer", "minimum": 0, "maximum": 10 } + }, + "additionalProperties": false + } + } + }'::jsonb, + 1, + true, + 'system' +) +ON CONFLICT DO NOTHING; + +COMMIT; diff --git a/projects/backend/services/config-service/poetry.lock b/projects/backend/services/config-service/poetry.lock new file mode 100644 index 00000000..15933a85 --- /dev/null +++ b/projects/backend/services/config-service/poetry.lock @@ -0,0 +1,2512 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "aio-pika" +version = "9.6.2" +description = "Wrapper around the aiormq for asyncio and humans" +optional = false +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f"}, + {file = "aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9"}, +] + +[package.dependencies] +aiormq = ">=6.8,<7" +yarl = "*" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, + {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, + {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, + {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, + {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, + {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, + {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, + {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, + {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, + {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, + {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, + {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, + {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, + {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiohttp-cors" +version = "0.7.0" +description = "CORS support for aiohttp" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"}, + {file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"}, +] + +[package.dependencies] +aiohttp = ">=1.1" + +[[package]] +name = "aiormq" +version = "6.9.4" +description = "Pure python AMQP asynchronous client library" +optional = false +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277"}, + {file = "aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef"}, +] + +[package.dependencies] +pamqp = "3.3.0" +yarl = "*" + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61"}, + {file = "asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8"}, + {file = "asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095"}, + {file = "asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9"}, + {file = "asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24"}, + {file = "asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20"}, + {file = "asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8"}, + {file = "asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602"}, + {file = "asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696"}, + {file = "asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d"}, + {file = "asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b"}, + {file = "asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9"}, + {file = "asyncpg-0.31.0-cp39-cp39-win32.whl", hash = "sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6"}, + {file = "asyncpg-0.31.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a"}, + {file = "asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735"}, +] + +[package.extras] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "backend-common" +version = "0.1.0" +description = "Common code shared between backend services" +optional = false +python-versions = "^3.12" +groups = ["main"] +files = [] +develop = true + +[package.dependencies] +aio-pika = ">=9.4.0" +aiohttp = "^3.13.3" +asyncpg = "^0.31.0" +prometheus-client = "^0.24.1" +pydantic-settings = "^2.4.0" +pyyaml = "^6.0.3" +structlog = "^24.2.0" + +[package.source] +type = "directory" +url = "../../common" + +[[package]] +name = "cached-property" +version = "2.0.1" +description = "A decorator for caching properties in classes." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb"}, + {file = "cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5"}, + {file = "googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1"}, +] + +[package.dependencies] +protobuf = ">=4.25.8,<8.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.3.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "librt" +version = "0.9.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443"}, + {file = "librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c"}, + {file = "librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2"}, + {file = "librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b"}, + {file = "librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774"}, + {file = "librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8"}, + {file = "librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671"}, + {file = "librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d"}, + {file = "librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882"}, + {file = "librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a"}, + {file = "librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6"}, + {file = "librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8"}, + {file = "librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a"}, + {file = "librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4"}, + {file = "librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d"}, + {file = "librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2"}, + {file = "librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f"}, + {file = "librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f"}, + {file = "librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745"}, + {file = "librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9"}, + {file = "librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e"}, + {file = "librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22"}, + {file = "librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11"}, + {file = "librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d"}, + {file = "librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd"}, + {file = "librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519"}, + {file = "librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5"}, + {file = "librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb"}, + {file = "librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499"}, + {file = "librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b"}, + {file = "librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9"}, + {file = "librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e"}, + {file = "librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15"}, + {file = "librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40"}, + {file = "librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118"}, + {file = "librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61"}, + {file = "librt-0.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4"}, + {file = "librt-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a"}, + {file = "librt-0.9.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7"}, + {file = "librt-0.9.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6"}, + {file = "librt-0.9.0-cp39-cp39-win32.whl", hash = "sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15"}, + {file = "librt-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1"}, + {file = "librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d"}, +] + +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[[package]] +name = "mypy" +version = "1.20.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "mypy-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3ba5d1e712ada9c3b6223dcbc5a31dac334ed62991e5caa17bcf5a4ddc349af0"}, + {file = "mypy-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e731284c117b0987fb1e6c5013a56f33e7faa1fce594066ab83876183ce1c66"}, + {file = "mypy-1.20.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e945b872a05f4fbefabe2249c0b07b6b194e5e11a86ebee9edf855de09806c"}, + {file = "mypy-1.20.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fc88acef0dc9b15246502b418980478c1bfc9702057a0e1e7598d01a7af8937"}, + {file = "mypy-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:14911a115c73608f155f648b978c5055d16ff974e6b1b5512d7fedf4fa8b15c6"}, + {file = "mypy-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:76d9b4c992cca3331d9793ef197ae360ea44953cf35beb2526e95b9e074f2866"}, + {file = "mypy-1.20.1-cp310-cp310-win_arm64.whl", hash = "sha256:b408722f80be44845da555671a5ef3a0c63f51ca5752b0c20e992dc9c0fbd3cd"}, + {file = "mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e"}, + {file = "mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca"}, + {file = "mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955"}, + {file = "mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8"}, + {file = "mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65"}, + {file = "mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2"}, + {file = "mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10"}, + {file = "mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51"}, + {file = "mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28"}, + {file = "mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f"}, + {file = "mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37"}, + {file = "mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237"}, + {file = "mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d"}, + {file = "mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019"}, + {file = "mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1"}, + {file = "mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184"}, + {file = "mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b"}, + {file = "mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e"}, + {file = "mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218"}, + {file = "mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2"}, + {file = "mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895"}, + {file = "mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12"}, + {file = "mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe"}, + {file = "mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08"}, + {file = "mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572"}, + {file = "mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6"}, + {file = "mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3"}, + {file = "mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4"}, + {file = "mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a"}, + {file = "mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986"}, + {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a"}, + {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9"}, + {file = "mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02"}, + {file = "mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa"}, + {file = "mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08"}, + {file = "mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06"}, + {file = "mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804"}, +] + +[package.dependencies] +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, + {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, +] + +[package.dependencies] +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464"}, +] + +[package.dependencies] +opentelemetry-proto = "1.39.1" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.39.1" +opentelemetry-proto = "1.39.1" +opentelemetry-sdk = ">=1.39.1,<1.40.0" +requests = ">=2.7,<3.0" +typing-extensions = ">=4.5.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d"}, + {file = "opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.60b1" +packaging = ">=18.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-aiohttp-server" +version = "0.60b1" +description = "Aiohttp server instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_aiohttp_server-0.60b1-py3-none-any.whl", hash = "sha256:97618c95c8020a4c5f1f846d2134470584ded25edd33b4d3fd425cf5f9d5a038"}, + {file = "opentelemetry_instrumentation_aiohttp_server-0.60b1.tar.gz", hash = "sha256:ca283014a103c97add5352839549e7572dbdeeeaf721e974316d226883fcc254"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.60b1" +opentelemetry-semantic-conventions = "0.60b1" +opentelemetry-util-http = "0.60b1" +wrapt = ">=1.0.0,<2.0.0" + +[package.extras] +instruments = ["aiohttp (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007"}, + {file = "opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8"}, +] + +[package.dependencies] +protobuf = ">=5.0,<7.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, + {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, +] + +[package.dependencies] +opentelemetry-api = "1.39.1" +opentelemetry-semantic-conventions = "0.60b1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, + {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, +] + +[package.dependencies] +opentelemetry-api = "1.39.1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199"}, + {file = "opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6"}, +] + +[[package]] +name = "packaging" +version = "26.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, +] + +[[package]] +name = "pamqp" +version = "3.3.0" +description = "RabbitMQ Focused AMQP low-level library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0"}, + {file = "pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b"}, +] + +[package.extras] +codegen = ["lxml", "requests", "yapf"] +testing = ["coverage", "flake8", "flake8-comprehensions", "flake8-deprecated", "flake8-import-order", "flake8-print", "flake8-quotes", "flake8-rst-docstrings", "flake8-tuple", "yapf"] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055"}, + {file = "prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9"}, +] + +[package.extras] +aiohttp = ["aiohttp"] +django = ["django"] +twisted = ["twisted"] + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"}, + {file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"}, + {file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"}, + {file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"}, + {file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"}, + {file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"}, + {file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1"}, + {file = "psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be"}, + {file = "psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b"}, + {file = "psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290"}, + {file = "psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd"}, + {file = "psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915"}, + {file = "psycopg2_binary-2.9.12-cp39-cp39-win_amd64.whl", hash = "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10"}, +] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"}, + {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.3" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"}, + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win32.whl", hash = "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"}, + {file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pydantic-settings" +version = "2.14.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e"}, + {file = "pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "types-boto3[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-aiohttp" +version = "1.1.0" +description = "Pytest plugin for aiohttp support" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d"}, + {file = "pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc"}, +] + +[package.dependencies] +aiohttp = ">=3.11.0b0" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==1.12.1)"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "requests" +version = "2.33.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "rpds-py" +version = "0.30.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] + +[[package]] +name = "ruff" +version = "0.5.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "structlog" +version = "24.4.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, + {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, +] + +[package.extras] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + +[[package]] +name = "types-jsonschema" +version = "4.26.0.20260408" +description = "Typing stubs for jsonschema" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_jsonschema-4.26.0.20260408-py3-none-any.whl", hash = "sha256:1ab0058c2612b0b81fc4e3c88ec3f9ad9b0af3fd31a176030d78b6d6cefb6e7b"}, + {file = "types_jsonschema-4.26.0.20260408.tar.gz", hash = "sha256:82b75a976ed1507c473b8dee2d4841bd65926758c6d672bb93d08bf5e16f1b3f"}, +] + +[package.dependencies] +referencing = "*" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "wrapt" +version = "1.17.3" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + +[[package]] +name = "yandex-taxi-testsuite" +version = "0.3.9" +description = "Yandex.Taxi Testsuite Package" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "yandex_taxi_testsuite-0.3.9-py3-none-any.whl", hash = "sha256:00c4126cd0cd80bcc1e345cfed81a47d6cfff811850d8517c9666e1d0907db1f"}, + {file = "yandex_taxi_testsuite-0.3.9.tar.gz", hash = "sha256:71c019843f92e9ec10abc3cd4f27bf725632889a15b632272b34b54e1a1baadf"}, +] + +[package.dependencies] +aiohttp = ">=3.5.4" +cached-property = ">=1.5.1" +packaging = "*" +psycopg2-binary = {version = ">=2.7.5", optional = true, markers = "extra == \"postgresql-binary\""} +py = ">=1.10" +pytest = ">=4.5.0" +pytest-aiohttp = ">=0.3.0" +pytest-asyncio = "<0.22.dev0 || >=0.25.dev0" +python-dateutil = ">=2.7.3" +PyYAML = ">=3.13" +yarl = ">=1.4.2,<1.6 || >1.6" + +[package.extras] +clickhouse = ["clickhouse-driver (>=0.2.0)"] +kafka = ["aiokafka (>=0.9.0)"] +mongodb = ["pymongo (>=3.7.1)"] +mysql = ["PyMySQL (>=0.9.2)"] +postgresql = ["psycopg2 (>=2.7.5)", "yandex-pgmigrate"] +postgresql-binary = ["psycopg2-binary (>=2.7.5)"] +rabbitmq = ["aio-pika (>=8.1.0)"] +redis = ["python-redis (>=0.2.1)", "redis (>=2.10.6)"] + +[[package]] +name = "yarl" +version = "1.23.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6"}, + {file = "yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d"}, + {file = "yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb"}, + {file = "yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2"}, + {file = "yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5"}, + {file = "yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46"}, + {file = "yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34"}, + {file = "yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d"}, + {file = "yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e"}, + {file = "yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543"}, + {file = "yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957"}, + {file = "yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3"}, + {file = "yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5"}, + {file = "yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595"}, + {file = "yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090"}, + {file = "yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe"}, + {file = "yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169"}, + {file = "yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70"}, + {file = "yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4"}, + {file = "yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4"}, + {file = "yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2"}, + {file = "yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25"}, + {file = "yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f"}, + {file = "yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[[package]] +name = "zipp" +version = "3.23.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, + {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "d1fef45258d94b3b97d8b917e20ef4744e0be4ebf46bff7e8d365a867f15d048" diff --git a/projects/backend/services/config-service/pyproject.toml b/projects/backend/services/config-service/pyproject.toml new file mode 100644 index 00000000..19410c84 --- /dev/null +++ b/projects/backend/services/config-service/pyproject.toml @@ -0,0 +1,53 @@ +[tool.poetry] +name = "config-service" +version = "0.1.0" +description = "Runtime configuration service: feature flags, QoS parameters, kill-switches" +authors = ["Platform Team"] +readme = "README.md" +packages = [{ include = "config_service", from = "src" }] + +[tool.poetry.dependencies] +python = "^3.12" +aiohttp = "^3.13.3" +aiohttp-cors = "^0.7.0" +asyncpg = "^0.31.0" +pydantic = "^2.4.0" +pydantic-settings = "^2.4.0" +structlog = "^24.2.0" +prometheus-client = "^0.24.1" +jsonschema = "^4.21.0" +opentelemetry-api = "^1.39.1" +opentelemetry-sdk = "^1.39.1" +opentelemetry-exporter-otlp-proto-http = "^1.39.1" +opentelemetry-instrumentation-aiohttp-server = "^0.60b0" +backend-common = { path = "../../common", develop = true } + +[tool.poetry.group.dev.dependencies] +pytest = "^8.2.2" +pytest-asyncio = "^0.21.1" +ruff = "^0.5.0" +mypy = "^1.10.0" +yandex-taxi-testsuite = { extras = ["postgresql-binary"], version = "^0.3.9" } +types-jsonschema = "^4.21.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.12" +strict = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +pythonpath = ["src", "."] diff --git a/projects/backend/services/config-service/src/config_service/__init__.py b/projects/backend/services/config-service/src/config_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/api/__init__.py b/projects/backend/services/config-service/src/config_service/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/api/router.py b/projects/backend/services/config-service/src/config_service/api/router.py new file mode 100644 index 00000000..db07b64a --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/api/router.py @@ -0,0 +1,14 @@ +"""Route registration.""" +from __future__ import annotations + +from aiohttp import web + +from config_service.api.routes.bulk import routes as bulk_routes +from config_service.api.routes.configs import routes as config_routes +from config_service.api.routes.schemas import routes as schema_routes + + +def setup_routes(app: web.Application) -> None: + app.add_routes(config_routes) + app.add_routes(bulk_routes) + app.add_routes(schema_routes) diff --git a/projects/backend/services/config-service/src/config_service/api/routes/__init__.py b/projects/backend/services/config-service/src/config_service/api/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/api/routes/bulk.py b/projects/backend/services/config-service/src/config_service/api/routes/bulk.py new file mode 100644 index 00000000..671511a2 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/api/routes/bulk.py @@ -0,0 +1,41 @@ +"""Bulk config endpoint for SDK polling.""" +from __future__ import annotations + +from email.utils import formatdate + +from aiohttp import web + +from config_service.prometheus_metrics import config_bulk_responses_total +from config_service.services.dependencies import get_bulk_service + +routes = web.RouteTableDef() + + +@routes.get("/api/v1/configs/bulk") +async def bulk_configs(request: web.Request) -> web.Response: + service_name = request.rel_url.query.get("service") + if not service_name: + raise web.HTTPBadRequest(reason="service query parameter is required") + + project_id = request.rel_url.query.get("project") + + bulk_svc = await get_bulk_service(request) + result = await bulk_svc.get_bulk(service_name, project_id) + + if_none_match = request.headers.get("If-None-Match", "").strip() + if if_none_match and if_none_match == result.etag: + config_bulk_responses_total.labels(status="304").inc() + headers: dict[str, str] = {"ETag": result.etag} + if result.last_modified: + headers["Last-Modified"] = formatdate( + result.last_modified.timestamp(), usegmt=True + ) + return web.Response(status=304, headers=headers) + + config_bulk_responses_total.labels(status="200").inc() + headers = {"ETag": result.etag} + if result.last_modified: + headers["Last-Modified"] = formatdate( + result.last_modified.timestamp(), usegmt=True + ) + return web.json_response({"configs": result.configs}, headers=headers) diff --git a/projects/backend/services/config-service/src/config_service/api/routes/configs.py b/projects/backend/services/config-service/src/config_service/api/routes/configs.py new file mode 100644 index 00000000..4c6d4413 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/api/routes/configs.py @@ -0,0 +1,594 @@ +"""Config CRUD endpoints.""" +from __future__ import annotations + +import json +from datetime import datetime, timezone +from uuid import UUID, uuid4 + +from aiohttp import web + +from config_service.core.exceptions import ( + ConfigNotFoundError, + ConfigValidationError, + IdempotencyConflictError, + VersionConflictError, +) +from config_service.domain.dto import ( + ActivateDeactivateRequest, + ConfigCreate, + ConfigPatch, + ConfigResponse, + RollbackRequest, +) +from config_service.domain.enums import ConfigType +from config_service.prometheus_metrics import config_optimistic_lock_conflicts_total +from config_service.services.dependencies import ( + get_audit_service, + get_config_service, + get_idempotency_service, + get_validation_service, + require_current_user, + ensure_permission, +) + +routes = web.RouteTableDef() + +_IDEMPOTENCY_KEY_HEADER = "Idempotency-Key" + + +def _correlation_id(request: web.Request) -> str | None: + return request.headers.get("X-Trace-Id") or request.headers.get("X-Correlation-Id") + + +def _source_ip(request: web.Request) -> str | None: + return request.headers.get("X-Forwarded-For") or request.remote + + +def _extract_version(request: web.Request, body: dict[str, object]) -> tuple[int, int]: + if_match = request.headers.get("If-Match") + if not if_match: + raise web.HTTPPreconditionRequired( + reason="If-Match header and version in body are required" + ) + body_version = body.get("version") + if body_version is None: + raise web.HTTPPreconditionRequired(reason="version field is required in request body") + + header_version = if_match.strip('"') + try: + header_version_int = int(header_version) + except ValueError: + raise web.HTTPBadRequest(reason="If-Match header must be a numeric version") + + try: + body_version_int = int(str(body_version)) + except (TypeError, ValueError): + raise web.HTTPBadRequest(reason="version in body must be an integer") + if header_version_int != body_version_int: + raise web.HTTPBadRequest( + reason="version mismatch between If-Match header and request body" + ) + return header_version_int, body_version_int + + +def _extract_version_from_query(request: web.Request) -> int: + if_match = request.headers.get("If-Match") + version_str = request.rel_url.query.get("version") + if not if_match or not version_str: + raise web.HTTPPreconditionRequired( + reason="If-Match header and version query parameter are required" + ) + try: + header_v = int(if_match.strip('"')) + query_v = int(version_str) + except ValueError: + raise web.HTTPBadRequest(reason="version must be an integer") + if header_v != query_v: + raise web.HTTPBadRequest( + reason="version mismatch between If-Match header and version query param" + ) + return header_v + + +def _config_to_response(config: object, redact: bool) -> dict[str, object]: + resp: dict[str, object] = ConfigResponse.model_validate( + config, from_attributes=True + ).model_dump(mode="json") + if redact: + resp["value"] = "***" + return resp + + +@routes.post("/api/v1/config") +async def create_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.create") + + body = await request.json() + dry_run = request.rel_url.query.get("dry_run", "false").lower() == "true" + + try: + dto = ConfigCreate.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + # Dry-run: validate schema but do NOT write to DB + if dry_run: + validation_svc = await get_validation_service(request) + try: + await validation_svc.validate_strict(dto.config_type, dto.value) + except ConfigValidationError as exc: + raise web.HTTPUnprocessableEntity( + text=json.dumps({"error": "Validation failed", "details": exc.errors}), + content_type="application/json", + ) + now = datetime.now(tz=timezone.utc).isoformat() + redact = dto.is_sensitive and "configs.sensitive.read" not in user.system_permissions + preview: dict[str, object] = { + "id": str(uuid4()), + "service_name": dto.service_name, + "project_id": dto.project_id, + "key": dto.key, + "config_type": dto.config_type.value, + "description": dto.description, + "value": "***" if redact else dto.value, + "metadata": dto.metadata, + "is_active": True, + "is_critical": dto.is_critical, + "is_sensitive": dto.is_sensitive, + "version": 1, + "created_by": user.user_id, + "updated_by": user.user_id, + "created_at": now, + "updated_at": now, + "deleted_at": None, + } + return web.json_response({"preview": preview, "dry_run": True}, status=200) + + svc = await get_config_service(request) + idempotency_svc = await get_idempotency_service(request) + audit_svc = await get_audit_service(request) + idempotency_key = request.headers.get(_IDEMPOTENCY_KEY_HEADER) + + if idempotency_key: + body_hash = idempotency_svc.body_hash(body) + try: + cached = await idempotency_svc.get_cached_response( + idempotency_key, user.user_id, request.path, body_hash + ) + except IdempotencyConflictError: + raise web.HTTPConflict(reason="Idempotency key reused with different payload") + if cached is not None: + return idempotency_svc.build_response(cached) + + try: + config = await svc.create( + service_name=dto.service_name, + project_id=dto.project_id, + key=dto.key, + config_type=dto.config_type, + description=dto.description, + value=dto.value, + metadata=dto.metadata, + is_critical=dto.is_critical, + is_sensitive=dto.is_sensitive, + created_by=user.user_id, + change_reason=dto.change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + ) + except ConfigValidationError as exc: + raise web.HTTPUnprocessableEntity( + text=json.dumps({"error": "Validation failed", "details": exc.errors}), + content_type="application/json", + ) + + audit_svc.log( + action="create", + actor=user.user_id, + service_name=config.service_name, + config_type=config.config_type.value, + config_id=str(config.id), + key=config.key, + change_reason=dto.change_reason, + is_critical=config.is_critical, + is_sensitive=config.is_sensitive, + correlation_id=_correlation_id(request), + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + value=config.value, + ) + + redact = config.is_sensitive and "configs.sensitive.read" not in user.system_permissions + resp_body = _config_to_response(config, redact) + + if idempotency_key: + body_hash = idempotency_svc.body_hash(body) + await idempotency_svc.store_response( + idempotency_key, user.user_id, request.path, body_hash, 201, resp_body + ) + + return web.json_response(resp_body, status=201, headers={"ETag": f'"{config.version}"'}) + + +@routes.get("/api/v1/config") +async def list_configs(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.view") + + q = request.rel_url.query + service_name = q.get("service") + project_id = q.get("project") + config_type_str = q.get("config_type") + is_active_str = q.get("is_active") + limit = min(int(q.get("limit", "50")), 500) + cursor = q.get("cursor") + + config_type = ConfigType(config_type_str) if config_type_str else None + is_active = {"true": True, "false": False}.get(is_active_str or "") if is_active_str else None + + svc = await get_config_service(request) + items, next_cursor = await svc.list_configs( + service_name=service_name, + project_id=project_id, + config_type=config_type, + is_active=is_active, + limit=limit, + cursor=cursor, + ) + + can_read_sensitive = user.is_superadmin or "configs.sensitive.read" in user.system_permissions + result = [_config_to_response(c, c.is_sensitive and not can_read_sensitive) for c in items] + return web.json_response({"items": result, "next_cursor": next_cursor}) + + +@routes.get("/api/v1/config/{config_id}") +async def get_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.view") + + config_id = UUID(request.match_info["config_id"]) + svc = await get_config_service(request) + + try: + config = await svc.get(config_id) + except ConfigNotFoundError: + raise web.HTTPNotFound() + + can_read_sensitive = user.is_superadmin or "configs.sensitive.read" in user.system_permissions + redact = config.is_sensitive and not can_read_sensitive + return web.json_response( + _config_to_response(config, redact), + headers={"ETag": f'"{config.version}"'}, + ) + + +@routes.patch("/api/v1/config/{config_id}") +async def patch_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.update") + + config_id = UUID(request.match_info["config_id"]) + body = await request.json() + dry_run = request.rel_url.query.get("dry_run", "false").lower() == "true" + + expected_version, _ = _extract_version(request, body) + + try: + dto = ConfigPatch.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + svc = await get_config_service(request) + audit_svc = await get_audit_service(request) + + # Dry-run: validate but do NOT write to DB + if dry_run: + try: + current = await svc.get(config_id) + except ConfigNotFoundError: + raise web.HTTPNotFound() + if dto.value is not None: + validation_svc = await get_validation_service(request) + try: + await validation_svc.validate_strict(current.config_type, dto.value) + except ConfigValidationError as exc: + raise web.HTTPUnprocessableEntity( + text=json.dumps({"error": "Validation failed", "details": exc.errors}), + content_type="application/json", + ) + merged_value = dto.value if dto.value is not None else current.value + merged_meta = dto.metadata if dto.metadata is not None else current.metadata + merged_active = dto.is_active if dto.is_active is not None else current.is_active + can_read_sensitive = user.is_superadmin or "configs.sensitive.read" in user.system_permissions + redact = current.is_sensitive and not can_read_sensitive + preview = { + "id": str(current.id), + "service_name": current.service_name, + "project_id": current.project_id, + "key": current.key, + "config_type": current.config_type.value, + "description": dto.description if dto.description is not None else current.description, + "value": "***" if redact else merged_value, + "metadata": merged_meta, + "is_active": merged_active, + "is_critical": dto.is_critical if dto.is_critical is not None else current.is_critical, + "is_sensitive": dto.is_sensitive if dto.is_sensitive is not None else current.is_sensitive, + "version": current.version + 1, + "created_by": current.created_by, + "updated_by": user.user_id, + "created_at": current.created_at.isoformat(), + "updated_at": datetime.now(tz=timezone.utc).isoformat(), + "deleted_at": None, + } + return web.json_response({"preview": preview, "dry_run": True}) + + try: + config = await svc.patch( + config_id, + expected_version, + changed_by=user.user_id, + change_reason=dto.change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + description=dto.description, + value=dto.value, + metadata=dto.metadata, + is_active=dto.is_active, + is_critical=dto.is_critical, + is_sensitive=dto.is_sensitive, + ) + except ConfigNotFoundError: + raise web.HTTPNotFound() + except VersionConflictError: + config_optimistic_lock_conflicts_total.labels(route="PATCH /api/v1/config/{id}").inc() + raise web.HTTPPreconditionFailed(reason="Version conflict — please re-fetch and retry") + except ConfigValidationError as exc: + raise web.HTTPUnprocessableEntity( + text=json.dumps({"error": "Validation failed", "details": exc.errors}), + content_type="application/json", + ) + + audit_svc.log( + action="patch", + actor=user.user_id, + service_name=config.service_name, + config_type=config.config_type.value, + config_id=str(config.id), + key=config.key, + change_reason=dto.change_reason, + is_critical=config.is_critical, + is_sensitive=config.is_sensitive, + correlation_id=_correlation_id(request), + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + value=config.value, + ) + + can_read_sensitive = user.is_superadmin or "configs.sensitive.read" in user.system_permissions + redact = config.is_sensitive and not can_read_sensitive + resp_body = _config_to_response(config, redact) + + return web.json_response(resp_body, headers={"ETag": f'"{config.version}"'}) + + +@routes.delete("/api/v1/config/{config_id}") +async def delete_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.delete") + + config_id = UUID(request.match_info["config_id"]) + expected_version = _extract_version_from_query(request) + change_reason = request.rel_url.query.get("change_reason", "").strip() + if not change_reason: + raise web.HTTPBadRequest(reason="change_reason query parameter is required") + + svc = await get_config_service(request) + audit_svc = await get_audit_service(request) + + try: + config = await svc.soft_delete( + config_id, + expected_version, + deleted_by=user.user_id, + change_reason=change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + ) + except ConfigNotFoundError: + raise web.HTTPNotFound() + except VersionConflictError: + config_optimistic_lock_conflicts_total.labels(route="DELETE /api/v1/config/{id}").inc() + raise web.HTTPPreconditionFailed(reason="Version conflict — please re-fetch and retry") + + audit_svc.log( + action="delete", + actor=user.user_id, + service_name=config.service_name, + config_type=config.config_type.value, + config_id=str(config.id), + key=config.key, + change_reason=change_reason, + is_critical=config.is_critical, + is_sensitive=config.is_sensitive, + correlation_id=_correlation_id(request), + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + ) + return web.Response(status=204) + + +@routes.post("/api/v1/config/{config_id}/activate") +async def activate_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.activate") + + config_id = UUID(request.match_info["config_id"]) + body = await request.json() + expected_version, _ = _extract_version(request, body) + + try: + dto = ActivateDeactivateRequest.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + svc = await get_config_service(request) + try: + config = await svc.patch( + config_id, + expected_version, + changed_by=user.user_id, + change_reason=dto.change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + is_active=True, + ) + except ConfigNotFoundError: + raise web.HTTPNotFound() + except VersionConflictError: + config_optimistic_lock_conflicts_total.labels(route="POST activate").inc() + raise web.HTTPPreconditionFailed(reason="Version conflict") + + return web.json_response( + _config_to_response(config, False), + headers={"ETag": f'"{config.version}"'}, + ) + + +@routes.post("/api/v1/config/{config_id}/deactivate") +async def deactivate_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.activate") + + config_id = UUID(request.match_info["config_id"]) + body = await request.json() + expected_version, _ = _extract_version(request, body) + + try: + dto = ActivateDeactivateRequest.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + svc = await get_config_service(request) + try: + config = await svc.patch( + config_id, + expected_version, + changed_by=user.user_id, + change_reason=dto.change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + is_active=False, + ) + except ConfigNotFoundError: + raise web.HTTPNotFound() + except VersionConflictError: + config_optimistic_lock_conflicts_total.labels(route="POST deactivate").inc() + raise web.HTTPPreconditionFailed(reason="Version conflict") + + return web.json_response( + _config_to_response(config, False), + headers={"ETag": f'"{config.version}"'}, + ) + + +@routes.post("/api/v1/config/{config_id}/rollback") +async def rollback_config(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.rollback") + + config_id = UUID(request.match_info["config_id"]) + body = await request.json() + expected_version, _ = _extract_version(request, body) + + try: + dto = RollbackRequest.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + svc = await get_config_service(request) + audit_svc = await get_audit_service(request) + + try: + config = await svc.rollback( + config_id, + expected_version, + dto.target_version, + changed_by=user.user_id, + change_reason=dto.change_reason, + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + correlation_id=_correlation_id(request), + ) + except ConfigNotFoundError: + raise web.HTTPNotFound() + except VersionConflictError: + config_optimistic_lock_conflicts_total.labels(route="POST rollback").inc() + raise web.HTTPPreconditionFailed(reason="Version conflict") + except ConfigValidationError as exc: + raise web.HTTPUnprocessableEntity( + text=json.dumps({"error": "Validation failed", "details": exc.errors}), + content_type="application/json", + ) + + audit_svc.log( + action="rollback", + actor=user.user_id, + service_name=config.service_name, + config_type=config.config_type.value, + config_id=str(config.id), + key=config.key, + change_reason=dto.change_reason, + is_critical=config.is_critical, + is_sensitive=config.is_sensitive, + correlation_id=_correlation_id(request), + source_ip=_source_ip(request), + user_agent=request.headers.get("User-Agent"), + value=config.value, + ) + + return web.json_response( + _config_to_response(config, False), + headers={"ETag": f'"{config.version}"'}, + ) + + +@routes.get("/api/v1/config/{config_id}/history") +async def get_history(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.view") + + config_id = UUID(request.match_info["config_id"]) + limit = min(int(request.rel_url.query.get("limit", "50")), 500) + offset = int(request.rel_url.query.get("offset", "0")) + + svc = await get_config_service(request) + try: + history = await svc.get_history(config_id, limit, offset) + except ConfigNotFoundError: + raise web.HTTPNotFound() + + items = [] + for h in history: + items.append({ + "id": str(h.id), + "config_id": str(h.config_id), + "version": h.version, + "service_name": h.service_name, + "key": h.key, + "config_type": h.config_type.value, + "value": h.value, + "metadata": h.metadata, + "is_active": h.is_active, + "changed_by": h.changed_by, + "change_reason": h.change_reason, + "correlation_id": h.correlation_id, + "changed_at": h.changed_at.isoformat(), + }) + + return web.json_response({"items": items}) diff --git a/projects/backend/services/config-service/src/config_service/api/routes/health.py b/projects/backend/services/config-service/src/config_service/api/routes/health.py new file mode 100644 index 00000000..df8d041e --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/api/routes/health.py @@ -0,0 +1,22 @@ +"""Health check endpoint.""" +from __future__ import annotations + +from aiohttp import web + +from backend_common.db.pool import get_pool + +health_routes = web.RouteTableDef() + + +@health_routes.get("/health") +async def health(request: web.Request) -> web.Response: + try: + pool = await get_pool() + async with pool.acquire() as conn: + await conn.fetchval("SELECT 1") + db_status = "ok" + except Exception: + db_status = "error" + + status = "ok" if db_status == "ok" else "degraded" + return web.json_response({"status": status, "db": db_status}) diff --git a/projects/backend/services/config-service/src/config_service/api/routes/schemas.py b/projects/backend/services/config-service/src/config_service/api/routes/schemas.py new file mode 100644 index 00000000..b61594ab --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/api/routes/schemas.py @@ -0,0 +1,106 @@ +"""Schema management endpoints.""" +from __future__ import annotations + +from aiohttp import web + +from config_service.core.exceptions import ( + SchemaBreakingChangeError, + SchemaSanityFailedError, +) +from config_service.domain.dto import SchemaUpdateRequest +from config_service.domain.enums import ConfigType +from config_service.domain.models import ConfigSchema +from config_service.services.dependencies import ( + get_schema_service, + get_validation_service, + require_current_user, + ensure_permission, +) + +routes = web.RouteTableDef() + + +def _schema_to_dict(s: ConfigSchema) -> dict[str, object]: + return { + "id": str(s.id), + "config_type": s.config_type.value, + "schema": s.schema, + "version": s.version, + "is_active": s.is_active, + "created_by": s.created_by, + "created_at": s.created_at.isoformat(), + } + + +@routes.get("/api/v1/schemas") +async def list_schemas(request: web.Request) -> web.Response: + require_current_user(request) + svc = await get_schema_service(request) + schemas = await svc.list_active() + return web.json_response({"items": [_schema_to_dict(s) for s in schemas]}) + + +@routes.get("/api/v1/schemas/{config_type}") +async def get_schema(request: web.Request) -> web.Response: + require_current_user(request) + try: + config_type = ConfigType(request.match_info["config_type"]) + except ValueError: + raise web.HTTPBadRequest(reason="Invalid config_type") + + svc = await get_schema_service(request) + schema = await svc.get_active(config_type) + if schema is None: + raise web.HTTPNotFound() + + return web.json_response(_schema_to_dict(schema)) + + +@routes.get("/api/v1/schemas/{config_type}/history") +async def get_schema_history(request: web.Request) -> web.Response: + require_current_user(request) + try: + config_type = ConfigType(request.match_info["config_type"]) + except ValueError: + raise web.HTTPBadRequest(reason="Invalid config_type") + + svc = await get_schema_service(request) + history = await svc.list_history(config_type) + return web.json_response({"items": [_schema_to_dict(s) for s in history]}) + + +@routes.put("/api/v1/schemas/{config_type}") +async def update_schema(request: web.Request) -> web.Response: + user = require_current_user(request) + ensure_permission(user, "configs.schemas.manage") + + try: + config_type = ConfigType(request.match_info["config_type"]) + except ValueError: + raise web.HTTPBadRequest(reason="Invalid config_type") + + body = await request.json() + try: + dto = SchemaUpdateRequest.model_validate(body) + except Exception as exc: + raise web.HTTPUnprocessableEntity(reason=str(exc).replace("\n", " ").replace("\r", "")) + + svc = await get_schema_service(request) + validation_svc = await get_validation_service(request) + + try: + schema = await svc.update(config_type, dto.schema_, created_by=user.user_id) + except SchemaBreakingChangeError as exc: + raise web.HTTPUnprocessableEntity( + text=str({"error": "Breaking schema changes", "violations": exc.violations}), + content_type="application/json", + ) + except SchemaSanityFailedError as exc: + raise web.HTTPInternalServerError( + text=str({"error": "Schema sanity check failed", "failures": exc.failures}), + content_type="application/json", + ) + + validation_svc.invalidate_cache(config_type) + + return web.json_response(_schema_to_dict(schema), status=200) diff --git a/projects/backend/services/config-service/src/config_service/core/__init__.py b/projects/backend/services/config-service/src/config_service/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/core/exceptions.py b/projects/backend/services/config-service/src/config_service/core/exceptions.py new file mode 100644 index 00000000..c9f415eb --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/core/exceptions.py @@ -0,0 +1,58 @@ +"""Domain exceptions for config-service.""" +from __future__ import annotations + + +class ConfigNotFoundError(Exception): + def __init__(self, config_id: str) -> None: + super().__init__(f"Config not found: {config_id}") + self.config_id = config_id + + +class VersionConflictError(Exception): + """Raised when optimistic lock check fails (version mismatch).""" + + def __init__(self, config_id: str, expected: int, actual: int) -> None: + super().__init__( + f"Version conflict for config {config_id}: expected {expected}, got {actual}" + ) + self.config_id = config_id + self.expected = expected + self.actual = actual + + +class SchemaBreakingChangeError(Exception): + """Raised when a schema update contains breaking (non-additive) changes.""" + + def __init__(self, violations: list[str]) -> None: + super().__init__(f"Breaking schema changes: {violations}") + self.violations = violations + + +class SchemaSanityFailedError(Exception): + """Raised when existing configs fail validation against new schema.""" + + def __init__(self, failures: list[dict[str, object]]) -> None: + super().__init__(f"Schema sanity check failed for {len(failures)} configs") + self.failures = failures + + +class IdempotencyConflictError(Exception): + """Raised when an idempotency key is reused with a different request body.""" + + def __init__(self, key: str) -> None: + super().__init__(f"Idempotency key conflict: {key}") + self.key = key + + +class ConfigValidationError(Exception): + """Raised when a config value fails JSON Schema validation.""" + + def __init__(self, errors: list[str]) -> None: + super().__init__(f"Config validation failed: {errors}") + self.errors = errors + + +class SchemaNotFoundError(Exception): + def __init__(self, config_type: str) -> None: + super().__init__(f"Schema not found for type: {config_type}") + self.config_type = config_type diff --git a/projects/backend/services/config-service/src/config_service/domain/__init__.py b/projects/backend/services/config-service/src/config_service/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/domain/dto.py b/projects/backend/services/config-service/src/config_service/domain/dto.py new file mode 100644 index 00000000..e0ede0b6 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/domain/dto.py @@ -0,0 +1,130 @@ +"""Pydantic DTOs for config-service API.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any +from uuid import UUID + +from pydantic import BaseModel, Field, field_validator + +from config_service.domain.enums import ConfigType + + +class ConfigCreate(BaseModel): + service_name: str = Field(min_length=1, max_length=128) + project_id: str | None = None + key: str = Field(min_length=1, max_length=128) + config_type: ConfigType + description: str | None = None + value: dict[str, Any] + metadata: dict[str, Any] = Field(default_factory=dict) + is_critical: bool = False + is_sensitive: bool = False + change_reason: str | None = None + + +class ConfigPatch(BaseModel): + version: int + change_reason: str = Field(min_length=1) + description: str | None = None + value: dict[str, Any] | None = None + metadata: dict[str, Any] | None = None + is_active: bool | None = None + is_critical: bool | None = None + is_sensitive: bool | None = None + + +class ActivateDeactivateRequest(BaseModel): + version: int + change_reason: str = Field(min_length=1) + + +class RollbackRequest(BaseModel): + version: int + target_version: int + change_reason: str = Field(min_length=1) + + @field_validator("target_version") + @classmethod + def target_must_differ(cls, v: int, info: Any) -> int: + current = info.data.get("version") + if current is not None and v == current: + raise ValueError("target_version must differ from current version") + return v + + +class SchemaUpdateRequest(BaseModel): + schema_: dict[str, Any] = Field(alias="schema") + + model_config = {"populate_by_name": True} + + +# ─── Response DTOs ──────────────────────────────────────────────────────────── + + +class ConfigResponse(BaseModel): + id: UUID + service_name: str + project_id: str | None + key: str + config_type: ConfigType + description: str | None + value: dict[str, Any] + metadata: dict[str, Any] + is_active: bool + is_critical: bool + is_sensitive: bool + version: int + created_by: str + updated_by: str + created_at: datetime + updated_at: datetime + deleted_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class ConfigHistoryResponse(BaseModel): + id: UUID + config_id: UUID + version: int + service_name: str + key: str + config_type: ConfigType + value: dict[str, Any] + metadata: dict[str, Any] + is_active: bool + changed_by: str + change_reason: str | None + source_ip: str | None + user_agent: str | None + correlation_id: str | None + changed_at: datetime + + model_config = {"from_attributes": True} + + +class SchemaResponse(BaseModel): + id: UUID + config_type: ConfigType + schema_: dict[str, Any] = Field(alias="schema") + version: int + is_active: bool + created_by: str + created_at: datetime + + model_config = {"from_attributes": True, "populate_by_name": True} + + +class BulkResponse(BaseModel): + configs: dict[str, Any] + + +class ListResponse(BaseModel): + items: list[ConfigResponse] + next_cursor: str | None + + +class DryRunResponse(BaseModel): + preview: ConfigResponse + dry_run: bool = True diff --git a/projects/backend/services/config-service/src/config_service/domain/enums.py b/projects/backend/services/config-service/src/config_service/domain/enums.py new file mode 100644 index 00000000..7f92987e --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/domain/enums.py @@ -0,0 +1,9 @@ +"""Domain enumerations.""" +from __future__ import annotations + +from enum import Enum + + +class ConfigType(str, Enum): + feature_flag = "feature_flag" + qos = "qos" diff --git a/projects/backend/services/config-service/src/config_service/domain/models.py b/projects/backend/services/config-service/src/config_service/domain/models.py new file mode 100644 index 00000000..bf5f7004 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/domain/models.py @@ -0,0 +1,146 @@ +"""Domain dataclasses — plain Python objects, no framework deps.""" +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any +from uuid import UUID + +from config_service.domain.enums import ConfigType + + +def _decode_jsonb(v: Any) -> Any: + return json.loads(v) if isinstance(v, str) else v + + +@dataclass +class Config: + id: UUID + service_name: str + project_id: str | None + key: str + config_type: ConfigType + description: str | None + value: dict[str, Any] + metadata: dict[str, Any] + is_active: bool + is_critical: bool + is_sensitive: bool + version: int + created_by: str + updated_by: str + created_at: datetime + updated_at: datetime + deleted_at: datetime | None + + @classmethod + def from_row(cls, row: dict[str, Any]) -> Config: + return cls( + id=row["id"], + service_name=row["service_name"], + project_id=row["project_id"], + key=row["key"], + config_type=ConfigType(row["config_type"]), + description=row["description"], + value=_decode_jsonb(row["value"]), + metadata=_decode_jsonb(row["metadata"]) or {}, + is_active=row["is_active"], + is_critical=row["is_critical"], + is_sensitive=row["is_sensitive"], + version=row["version"], + created_by=row["created_by"], + updated_by=row["updated_by"], + created_at=row["created_at"], + updated_at=row["updated_at"], + deleted_at=row.get("deleted_at"), + ) + + +@dataclass +class ConfigHistory: + id: UUID + config_id: UUID + version: int + service_name: str + key: str + config_type: ConfigType + value: dict[str, Any] + metadata: dict[str, Any] + is_active: bool + changed_by: str + change_reason: str | None + source_ip: str | None + user_agent: str | None + correlation_id: str | None + changed_at: datetime + + @classmethod + def from_row(cls, row: dict[str, Any]) -> ConfigHistory: + return cls( + id=row["id"], + config_id=row["config_id"], + version=row["version"], + service_name=row["service_name"], + key=row["key"], + config_type=ConfigType(row["config_type"]), + value=_decode_jsonb(row["value"]), + metadata=_decode_jsonb(row["metadata"]) or {}, + is_active=row["is_active"], + changed_by=row["changed_by"], + change_reason=row.get("change_reason"), + source_ip=row.get("source_ip"), + user_agent=row.get("user_agent"), + correlation_id=row.get("correlation_id"), + changed_at=row["changed_at"], + ) + + +@dataclass +class ConfigSchema: + id: UUID + config_type: ConfigType + schema: dict[str, Any] + version: int + is_active: bool + created_by: str + created_at: datetime + + @classmethod + def from_row(cls, row: dict[str, Any]) -> ConfigSchema: + return cls( + id=row["id"], + config_type=ConfigType(row["config_type"]), + schema=_decode_jsonb(row["schema"]), + version=row["version"], + is_active=row["is_active"], + created_by=row["created_by"], + created_at=row["created_at"], + ) + + +@dataclass +class IdempotencyRecord: + id: UUID + idempotency_key: str + user_id: str + request_path: str + request_hash: str + response_status: int + response_body: dict[str, Any] + expires_at: datetime + created_at: datetime + + @classmethod + def from_row(cls, row: dict[str, Any]) -> IdempotencyRecord: + return cls( + id=row["id"], + idempotency_key=row["idempotency_key"], + user_id=row["user_id"], + request_path=row["request_path"], + request_hash=row["request_hash"], + response_status=row["response_status"], + response_body=_decode_jsonb(row["response_body"]), + expires_at=row["expires_at"], + created_at=row["created_at"], + ) diff --git a/projects/backend/services/config-service/src/config_service/main.py b/projects/backend/services/config-service/src/config_service/main.py new file mode 100644 index 00000000..4aa27415 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/main.py @@ -0,0 +1,64 @@ +"""aiohttp application entrypoint.""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from aiohttp import web + +from backend_common.aiohttp_app import add_cors_to_routes, add_openapi_spec, create_base_app +from backend_common.db.pool import close_pool_service as close_pool, init_pool_service +from backend_common.logging_config import configure_logging +from backend_common.metrics import metrics_handler, metrics_middleware +from backend_common.middleware.error_handler import error_handling_middleware + +from config_service.api.router import setup_routes +from config_service.api.routes.health import health_routes +from config_service.otel import setup_otel, shutdown_otel +from config_service.settings import settings +from config_service.workers import start_background_worker, stop_background_worker + +configure_logging() + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +OPENAPI_PATH = PROJECT_ROOT / "openapi" / "openapi.yaml" + + +async def init_pool(_app: Any = None) -> None: + await init_pool_service(_app, settings) + + +def create_app() -> web.Application: + app, cors = create_base_app(settings) + app.middlewares.append(error_handling_middleware) # type: ignore[arg-type] + app.middlewares.append(metrics_middleware("config-service")) + + app.add_routes(health_routes) + if OPENAPI_PATH.exists(): + add_openapi_spec(app, OPENAPI_PATH) + setup_routes(app) + app.router.add_get("/metrics", metrics_handler) + + setup_otel(app) + + app.on_startup.append(init_pool) + app.on_startup.append(start_background_worker) + app.on_cleanup.append(stop_background_worker) + app.on_cleanup.append(shutdown_otel) + app.on_cleanup.append(close_pool) + + add_cors_to_routes(app, cors) + return app + + +def main() -> None: + web.run_app( + create_app(), + host=settings.host, + port=settings.port, + access_log=None, + ) + + +if __name__ == "__main__": + main() diff --git a/projects/backend/services/config-service/src/config_service/otel.py b/projects/backend/services/config-service/src/config_service/otel.py new file mode 100644 index 00000000..0eff37a9 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/otel.py @@ -0,0 +1,54 @@ +"""OpenTelemetry instrumentation for config-service.""" +from __future__ import annotations + +import structlog +from aiohttp import web + +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource, SERVICE_NAME +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.aiohttp_server import AioHttpServerInstrumentor + +from config_service.settings import settings + +logger = structlog.get_logger(__name__) + +_provider: TracerProvider | None = None + + +def setup_otel(app: web.Application) -> None: + global _provider + + endpoint = settings.otel_exporter_endpoint + if not endpoint: + logger.info("otel_exporter_endpoint not set — OpenTelemetry tracing disabled") + return + + resource = Resource.create({SERVICE_NAME: settings.app_name}) + _provider = TracerProvider(resource=resource) + + exporter = OTLPSpanExporter(endpoint=f"{endpoint}/v1/traces") + _provider.add_span_processor(BatchSpanProcessor(exporter)) + + trace.set_tracer_provider(_provider) + AioHttpServerInstrumentor().instrument(server=app) + + logger.info( + "OpenTelemetry tracing enabled", + endpoint=str(endpoint), + service=settings.app_name, + ) + + +async def shutdown_otel(_app: web.Application) -> None: + global _provider + if _provider is not None: + _provider.shutdown() + logger.info("OpenTelemetry tracer provider shut down") + _provider = None + + +def get_tracer(name: str = __name__) -> trace.Tracer: + return trace.get_tracer(name) diff --git a/projects/backend/services/config-service/src/config_service/prometheus_metrics.py b/projects/backend/services/config-service/src/config_service/prometheus_metrics.py new file mode 100644 index 00000000..3a58d617 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/prometheus_metrics.py @@ -0,0 +1,46 @@ +"""Domain metrics for config-service.""" +from __future__ import annotations + +from prometheus_client import Counter, Histogram + +config_read_schema_violations_total = Counter( + "config_read_schema_violations_total", + "Number of schema violations detected on read (paranoid mode)", + ["config_type", "config_id"], +) + +config_compat_check_rejections_total = Counter( + "config_compat_check_rejections_total", + "Number of schema updates rejected by compat checker", + ["config_type", "rule"], +) + +config_sanity_check_failures_total = Counter( + "config_sanity_check_failures_total", + "Number of sanity check failures when updating schema", + ["config_type"], +) + +config_optimistic_lock_conflicts_total = Counter( + "config_optimistic_lock_conflicts_total", + "Number of 412 Precondition Failed responses due to version conflict", + ["route"], +) + +config_idempotency_hits_total = Counter( + "config_idempotency_hits_total", + "Idempotency key cache hits", + ["result"], # hit | conflict +) + +config_bulk_responses_total = Counter( + "config_bulk_responses_total", + "Responses from bulk endpoint", + ["status"], # 200 | 304 +) + +config_propagation_lag_seconds = Histogram( + "config_propagation_lag_seconds", + "Time between config update and SDK poll (from updated_at)", + buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0], +) diff --git a/projects/backend/services/config-service/src/config_service/repositories/__init__.py b/projects/backend/services/config-service/src/config_service/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/repositories/config_repo.py b/projects/backend/services/config-service/src/config_service/repositories/config_repo.py new file mode 100644 index 00000000..d8bcee01 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/repositories/config_repo.py @@ -0,0 +1,288 @@ +"""Config repository — CRUD with optimistic locking and history in one transaction.""" +from __future__ import annotations + +import json +from typing import Any +from uuid import UUID + +import asyncpg + +from backend_common.repositories.base import BaseRepository +from config_service.domain.enums import ConfigType +from config_service.domain.models import Config + + +class ConfigRepository(BaseRepository): + + async def create( + self, + *, + service_name: str, + project_id: str | None, + key: str, + config_type: ConfigType, + description: str | None, + value: dict[str, Any], + metadata: dict[str, Any], + is_critical: bool, + is_sensitive: bool, + created_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> Config: + async with self._pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow( + """ + INSERT INTO configs ( + service_name, project_id, key, config_type, description, + value, metadata, is_critical, is_sensitive, is_active, + version, created_by, updated_by + ) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8,$9,true,1,$10,$10) + RETURNING * + """, + service_name, project_id, key, config_type.value, description, + json.dumps(value), json.dumps(metadata), + is_critical, is_sensitive, created_by, + ) + config = Config.from_row(dict(row)) + await self._insert_history( + conn, + config=config, + changed_by=created_by, + change_reason=change_reason or "Initial creation", + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + ) + return config + + async def get_by_id(self, config_id: UUID) -> Config | None: + row = await self._fetchrow( + "SELECT * FROM configs WHERE id = $1 AND deleted_at IS NULL", + config_id, + ) + if row is None: + return None + return Config.from_row(dict(row)) + + async def list_by_filters( + self, + *, + service_name: str | None = None, + project_id: str | None = None, + config_type: ConfigType | None = None, + is_active: bool | None = None, + limit: int = 50, + cursor: str | None = None, + ) -> tuple[list[Config], str | None]: + conditions = ["deleted_at IS NULL"] + args: list[Any] = [] + idx = 1 + + if service_name is not None: + conditions.append(f"service_name = ${idx}") + args.append(service_name) + idx += 1 + if project_id is not None: + conditions.append(f"project_id = ${idx}") + args.append(project_id) + idx += 1 + if config_type is not None: + conditions.append(f"config_type = ${idx}") + args.append(config_type.value) + idx += 1 + if is_active is not None: + conditions.append(f"is_active = ${idx}") + args.append(is_active) + idx += 1 + if cursor is not None: + conditions.append(f"id > ${idx}::uuid") + args.append(cursor) + idx += 1 + + where = " AND ".join(conditions) + args.append(limit + 1) + rows = await self._fetch( + f"SELECT * FROM configs WHERE {where} ORDER BY id LIMIT ${idx}", + *args, + ) + items = [Config.from_row(dict(r)) for r in rows] + + next_cursor: str | None = None + if len(items) > limit: + items = items[:limit] + next_cursor = str(items[-1].id) + + return items, next_cursor + + async def update_with_version( + self, + *, + config_id: UUID, + expected_version: int, + changed_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + description: str | None = None, + value: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + is_active: bool | None = None, + is_critical: bool | None = None, + is_sensitive: bool | None = None, + ) -> Config | None: + """Returns None if version conflict (0 rows updated).""" + set_parts = ["version = version + 1", "updated_by = $3", "updated_at = NOW()"] + args: list[Any] = [config_id, expected_version, changed_by] + idx = 4 + + if description is not None: + set_parts.append(f"description = ${idx}") + args.append(description) + idx += 1 + if value is not None: + set_parts.append(f"value = ${idx}::jsonb") + args.append(json.dumps(value)) + idx += 1 + if metadata is not None: + set_parts.append(f"metadata = ${idx}::jsonb") + args.append(json.dumps(metadata)) + idx += 1 + if is_active is not None: + set_parts.append(f"is_active = ${idx}") + args.append(is_active) + idx += 1 + if is_critical is not None: + set_parts.append(f"is_critical = ${idx}") + args.append(is_critical) + idx += 1 + if is_sensitive is not None: + set_parts.append(f"is_sensitive = ${idx}") + args.append(is_sensitive) + idx += 1 + + set_clause = ", ".join(set_parts) + + async with self._pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow( + f""" + UPDATE configs + SET {set_clause} + WHERE id = $1 AND version = $2 AND deleted_at IS NULL + RETURNING * + """, + *args, + ) + if row is None: + return None + config = Config.from_row(dict(row)) + await self._insert_history( + conn, + config=config, + changed_by=changed_by, + change_reason=change_reason, + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + ) + return config + + async def soft_delete( + self, + *, + config_id: UUID, + expected_version: int, + deleted_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> Config | None: + async with self._pool.acquire() as conn: + async with conn.transaction(): + row = await conn.fetchrow( + """ + UPDATE configs + SET deleted_at = NOW(), version = version + 1, + updated_by = $3, updated_at = NOW() + WHERE id = $1 AND version = $2 AND deleted_at IS NULL + RETURNING * + """, + config_id, expected_version, deleted_by, + ) + if row is None: + return None + config = Config.from_row(dict(row)) + await self._insert_history( + conn, + config=config, + changed_by=deleted_by, + change_reason=change_reason, + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + ) + return config + + async def list_active_by_service( + self, + service_name: str, + project_id: str | None, + ) -> list[Config]: + if project_id is not None: + rows = await self._fetch( + """ + SELECT * FROM configs + WHERE service_name = $1 AND project_id = $2 + AND is_active = true AND deleted_at IS NULL + """, + service_name, project_id, + ) + else: + rows = await self._fetch( + """ + SELECT * FROM configs + WHERE service_name = $1 AND project_id IS NULL + AND is_active = true AND deleted_at IS NULL + """, + service_name, + ) + return [Config.from_row(dict(r)) for r in rows] + + async def list_by_type(self, config_type: ConfigType) -> list[Config]: + rows = await self._fetch( + "SELECT * FROM configs WHERE config_type = $1 AND deleted_at IS NULL", + config_type.value, + ) + return [Config.from_row(dict(r)) for r in rows] + + @staticmethod + async def _insert_history( + conn: asyncpg.Connection, + *, + config: Config, + changed_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> None: + await conn.execute( + """ + INSERT INTO config_history ( + config_id, version, service_name, key, config_type, + value, metadata, is_active, + changed_by, change_reason, source_ip, user_agent, correlation_id + ) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb,$8,$9,$10,$11,$12,$13) + """, + config.id, config.version, config.service_name, config.key, + config.config_type.value, + json.dumps(config.value), json.dumps(config.metadata), + config.is_active, + changed_by, change_reason, source_ip, user_agent, correlation_id, + ) diff --git a/projects/backend/services/config-service/src/config_service/repositories/history_repo.py b/projects/backend/services/config-service/src/config_service/repositories/history_repo.py new file mode 100644 index 00000000..e3545300 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/repositories/history_repo.py @@ -0,0 +1,36 @@ +"""Config history repository.""" +from __future__ import annotations + +from uuid import UUID + +from backend_common.repositories.base import BaseRepository +from config_service.domain.models import ConfigHistory + + +class HistoryRepository(BaseRepository): + + async def get_by_version(self, config_id: UUID, version: int) -> ConfigHistory | None: + row = await self._fetchrow( + "SELECT * FROM config_history WHERE config_id = $1 AND version = $2", + config_id, version, + ) + if row is None: + return None + return ConfigHistory.from_row(dict(row)) + + async def list_by_config_id( + self, + config_id: UUID, + limit: int = 50, + offset: int = 0, + ) -> list[ConfigHistory]: + rows = await self._fetch( + """ + SELECT * FROM config_history + WHERE config_id = $1 + ORDER BY version DESC + LIMIT $2 OFFSET $3 + """, + config_id, limit, offset, + ) + return [ConfigHistory.from_row(dict(r)) for r in rows] diff --git a/projects/backend/services/config-service/src/config_service/repositories/idempotency_repo.py b/projects/backend/services/config-service/src/config_service/repositories/idempotency_repo.py new file mode 100644 index 00000000..cbf1a3ef --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/repositories/idempotency_repo.py @@ -0,0 +1,75 @@ +"""Idempotency repository for config-service (TTL via expires_at).""" +from __future__ import annotations + +import json +from datetime import datetime +from typing import Any +from uuid import UUID + +from asyncpg import Record + +from backend_common.repositories.base import BaseRepository +from config_service.domain.models import IdempotencyRecord + + +class IdempotencyRepository(BaseRepository): + + async def get(self, key: str) -> IdempotencyRecord | None: + row = await self._fetchrow( + """ + SELECT * FROM idempotency_keys + WHERE idempotency_key = $1 AND expires_at > NOW() + """, + key, + ) + if row is None: + return None + return self._to_record(row) + + async def save( + self, + *, + key: str, + user_id: str, + request_path: str, + request_hash: str, + response_status: int, + response_body: dict[str, Any], + expires_at: datetime, + ) -> None: + await self._execute( + """ + INSERT INTO idempotency_keys ( + idempotency_key, user_id, request_path, request_hash, + response_status, response_body, expires_at + ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7) + ON CONFLICT (idempotency_key) DO NOTHING + """, + key, user_id, request_path, request_hash, + response_status, + json.dumps(response_body, sort_keys=True, separators=(",", ":"), default=str), + expires_at, + ) + + async def delete_expired(self) -> int: + result = await self._execute( + "DELETE FROM idempotency_keys WHERE expires_at <= NOW()" + ) + return int(result.split()[-1]) + + @staticmethod + def _to_record(row: Record) -> IdempotencyRecord: + body = row["response_body"] + if isinstance(body, str): + body = json.loads(body) + return IdempotencyRecord( + id=row["id"], + idempotency_key=row["idempotency_key"], + user_id=row["user_id"], + request_path=row["request_path"], + request_hash=row["request_hash"], + response_status=row["response_status"], + response_body=body, + expires_at=row["expires_at"], + created_at=row["created_at"], + ) diff --git a/projects/backend/services/config-service/src/config_service/repositories/schema_repo.py b/projects/backend/services/config-service/src/config_service/repositories/schema_repo.py new file mode 100644 index 00000000..6f44d896 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/repositories/schema_repo.py @@ -0,0 +1,68 @@ +"""Config schema repository.""" +from __future__ import annotations + +import json +from typing import Any + +import asyncpg + +from backend_common.repositories.base import BaseRepository +from config_service.domain.enums import ConfigType +from config_service.domain.models import ConfigSchema + + +class SchemaRepository(BaseRepository): + + async def get_active(self, config_type: ConfigType) -> ConfigSchema | None: + row = await self._fetchrow( + "SELECT * FROM config_schemas WHERE config_type = $1 AND is_active = true", + config_type.value, + ) + if row is None: + return None + return ConfigSchema.from_row(dict(row)) + + async def list_active(self) -> list[ConfigSchema]: + rows = await self._fetch( + "SELECT * FROM config_schemas WHERE is_active = true ORDER BY config_type" + ) + return [ConfigSchema.from_row(dict(r)) for r in rows] + + async def list_history(self, config_type: ConfigType) -> list[ConfigSchema]: + rows = await self._fetch( + "SELECT * FROM config_schemas WHERE config_type = $1 ORDER BY version DESC", + config_type.value, + ) + return [ConfigSchema.from_row(dict(r)) for r in rows] + + async def insert_new_version_and_activate( + self, + config_type: ConfigType, + schema: dict[str, Any], + created_by: str, + ) -> ConfigSchema: + async with self._pool.acquire() as conn: + async with conn.transaction(): + # Get next version number + result = await conn.fetchval( + "SELECT COALESCE(MAX(version), 0) + 1 FROM config_schemas WHERE config_type = $1", + config_type.value, + ) + new_version: int = result + + # Deactivate current active schema + await conn.execute( + "UPDATE config_schemas SET is_active = false WHERE config_type = $1 AND is_active = true", + config_type.value, + ) + + # Insert new active version + row = await conn.fetchrow( + """ + INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) + VALUES ($1, $2::jsonb, $3, true, $4) + RETURNING * + """, + config_type.value, json.dumps(schema), new_version, created_by, + ) + return ConfigSchema.from_row(dict(row)) diff --git a/projects/backend/services/config-service/src/config_service/services/__init__.py b/projects/backend/services/config-service/src/config_service/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/backend/services/config-service/src/config_service/services/audit_service.py b/projects/backend/services/config-service/src/config_service/services/audit_service.py new file mode 100644 index 00000000..7d4617c1 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/audit_service.py @@ -0,0 +1,44 @@ +"""Structured audit logging for config changes.""" +from __future__ import annotations + +from typing import Any + +import structlog + +logger = structlog.get_logger("audit") + + +class AuditService: + def log( + self, + *, + action: str, + actor: str, + service_name: str, + config_type: str, + config_id: str, + key: str, + change_reason: str | None, + is_critical: bool, + is_sensitive: bool, + correlation_id: str | None, + source_ip: str | None, + user_agent: str | None, + value: Any = None, + ) -> None: + redacted_value = "***" if is_sensitive else value + logger.info( + "config_audit", + action=action, + actor=actor, + service_name=service_name, + config_type=config_type, + config_id=config_id, + key=key, + change_reason=change_reason, + is_critical=is_critical, + value=redacted_value, + correlation_id=correlation_id, + source_ip=source_ip, + user_agent=user_agent, + ) diff --git a/projects/backend/services/config-service/src/config_service/services/bulk_service.py b/projects/backend/services/config-service/src/config_service/services/bulk_service.py new file mode 100644 index 00000000..b6345258 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/bulk_service.py @@ -0,0 +1,51 @@ +"""Bulk endpoint service: collects active configs, computes ETag.""" +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from config_service.repositories.config_repo import ConfigRepository +from config_service.services.validation_service import ValidationService + + +@dataclass +class BulkResult: + configs: dict[str, Any] + etag: str + last_modified: datetime | None + + +class BulkService: + def __init__( + self, + config_repo: ConfigRepository, + validation_service: ValidationService, + ) -> None: + self._config_repo = config_repo + self._validation = validation_service + + async def get_bulk( + self, + service_name: str, + project_id: str | None, + ) -> BulkResult: + configs = await self._config_repo.list_active_by_service(service_name, project_id) + + result: dict[str, Any] = {} + last_modified: datetime | None = None + + for cfg in configs: + await self._validation.validate_paranoid( + cfg.config_type, cfg.value, str(cfg.id) + ) + result[cfg.key] = cfg.value + if last_modified is None or cfg.updated_at > last_modified: + last_modified = cfg.updated_at + + serialized = json.dumps(result, sort_keys=True, separators=(",", ":"), default=str) + etag = f'W/"{hashlib.sha256(serialized.encode()).hexdigest()[:16]}"' + + return BulkResult(configs=result, etag=etag, last_modified=last_modified) diff --git a/projects/backend/services/config-service/src/config_service/services/config_service.py b/projects/backend/services/config-service/src/config_service/services/config_service.py new file mode 100644 index 00000000..962d6b14 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/config_service.py @@ -0,0 +1,208 @@ +"""Core config business logic: CRUD, rollback, dry-run, optimistic locking.""" +from __future__ import annotations + +from typing import Any +from uuid import UUID + +import structlog + +from config_service.core.exceptions import ( + ConfigNotFoundError, + VersionConflictError, +) +from config_service.domain.models import Config, ConfigHistory +from config_service.repositories.config_repo import ConfigRepository +from config_service.repositories.history_repo import HistoryRepository +from config_service.services.validation_service import ValidationService + +logger = structlog.get_logger(__name__) + + +class ConfigService: + def __init__( + self, + config_repo: ConfigRepository, + history_repo: HistoryRepository, + validation_service: ValidationService, + ) -> None: + self._config_repo = config_repo + self._history_repo = history_repo + self._validation = validation_service + + async def create( + self, + *, + service_name: str, + project_id: str | None, + key: str, + config_type: Any, + description: str | None, + value: dict[str, Any], + metadata: dict[str, Any], + is_critical: bool, + is_sensitive: bool, + created_by: str, + change_reason: str | None, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> Config: + await self._validation.validate_strict(config_type, value) + return await self._config_repo.create( + service_name=service_name, + project_id=project_id, + key=key, + config_type=config_type, + description=description, + value=value, + metadata=metadata, + is_critical=is_critical, + is_sensitive=is_sensitive, + created_by=created_by, + change_reason=change_reason or "Initial creation", + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + ) + + async def get(self, config_id: UUID) -> Config: + config = await self._config_repo.get_by_id(config_id) + if config is None: + raise ConfigNotFoundError(str(config_id)) + await self._validation.validate_paranoid( + config.config_type, config.value, str(config_id) + ) + return config + + async def list_configs( + self, + *, + service_name: str | None = None, + project_id: str | None = None, + config_type: Any = None, + is_active: bool | None = None, + limit: int = 50, + cursor: str | None = None, + ) -> tuple[list[Config], str | None]: + return await self._config_repo.list_by_filters( + service_name=service_name, + project_id=project_id, + config_type=config_type, + is_active=is_active, + limit=limit, + cursor=cursor, + ) + + async def patch( + self, + config_id: UUID, + expected_version: int, + *, + changed_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + description: str | None = None, + value: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + is_active: bool | None = None, + is_critical: bool | None = None, + is_sensitive: bool | None = None, + ) -> Config: + if value is not None: + existing = await self._config_repo.get_by_id(config_id) + if existing is None: + raise ConfigNotFoundError(str(config_id)) + await self._validation.validate_strict(existing.config_type, value) + + result = await self._config_repo.update_with_version( + config_id=config_id, + expected_version=expected_version, + changed_by=changed_by, + change_reason=change_reason, + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + description=description, + value=value, + metadata=metadata, + is_active=is_active, + is_critical=is_critical, + is_sensitive=is_sensitive, + ) + if result is None: + raise VersionConflictError(str(config_id), expected_version, -1) + return result + + async def soft_delete( + self, + config_id: UUID, + expected_version: int, + *, + deleted_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> Config: + result = await self._config_repo.soft_delete( + config_id=config_id, + expected_version=expected_version, + deleted_by=deleted_by, + change_reason=change_reason, + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + ) + if result is None: + config = await self._config_repo.get_by_id(config_id) + if config is None: + raise ConfigNotFoundError(str(config_id)) + raise VersionConflictError(str(config_id), expected_version, config.version) + return result + + async def rollback( + self, + config_id: UUID, + expected_version: int, + target_version: int, + *, + changed_by: str, + change_reason: str, + source_ip: str | None, + user_agent: str | None, + correlation_id: str | None, + ) -> Config: + target = await self._history_repo.get_by_version(config_id, target_version) + if target is None: + raise ConfigNotFoundError(f"{config_id} history version {target_version}") + + await self._validation.validate_strict(target.config_type, target.value) + + result = await self._config_repo.update_with_version( + config_id=config_id, + expected_version=expected_version, + changed_by=changed_by, + change_reason=change_reason, + source_ip=source_ip, + user_agent=user_agent, + correlation_id=correlation_id, + value=target.value, + metadata=target.metadata, + is_active=target.is_active, + ) + if result is None: + raise VersionConflictError(str(config_id), expected_version, -1) + return result + + async def get_history( + self, + config_id: UUID, + limit: int = 50, + offset: int = 0, + ) -> list[ConfigHistory]: + config = await self._config_repo.get_by_id(config_id) + if config is None: + raise ConfigNotFoundError(str(config_id)) + return await self._history_repo.list_by_config_id(config_id, limit, offset) diff --git a/projects/backend/services/config-service/src/config_service/services/dependencies.py b/projects/backend/services/config-service/src/config_service/services/dependencies.py new file mode 100644 index 00000000..332c5ab0 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/dependencies.py @@ -0,0 +1,129 @@ +"""Dependency injection and RBAC helpers.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Awaitable, Callable, TypeVar + +from aiohttp import web + +from backend_common.db.pool import get_pool +from config_service.repositories.config_repo import ConfigRepository +from config_service.repositories.history_repo import HistoryRepository +from config_service.repositories.idempotency_repo import IdempotencyRepository +from config_service.repositories.schema_repo import SchemaRepository +from config_service.services.audit_service import AuditService +from config_service.services.bulk_service import BulkService +from config_service.services.config_service import ConfigService +from config_service.services.idempotency import IdempotencyService +from config_service.services.schema_service import SchemaService +from config_service.services.validation_service import ValidationService + +TService = TypeVar("TService") + +_CONFIG_SVC_KEY = "config_service" +_SCHEMA_SVC_KEY = "schema_service" +_VALIDATION_SVC_KEY = "validation_service" +_BULK_SVC_KEY = "bulk_service" +_IDEMPOTENCY_SVC_KEY = "idempotency_service" +_AUDIT_SVC_KEY = "audit_service" + + +@dataclass(frozen=True) +class UserContext: + user_id: str + is_superadmin: bool + system_permissions: frozenset[str] + project_permissions: dict[str, list[str]] + + +def require_current_user(request: web.Request) -> UserContext: + user_id = request.headers.get("X-User-Id") + if not user_id: + raise web.HTTPUnauthorized(reason="Missing X-User-Id header") + is_superadmin = request.headers.get("X-User-Is-Superadmin", "false").lower() == "true" + raw_sys = request.headers.get("X-User-System-Permissions", "") + system_permissions = frozenset(p.strip() for p in raw_sys.split(",") if p.strip()) + raw_proj = request.headers.get("X-User-Permissions", "") + project_permissions: dict[str, list[str]] = {} + for entry in raw_proj.split(";"): + entry = entry.strip() + if ":" in entry: + proj, perms = entry.split(":", 1) + project_permissions[proj.strip()] = [p.strip() for p in perms.split(",") if p.strip()] + return UserContext( + user_id=user_id, + is_superadmin=is_superadmin, + system_permissions=system_permissions, + project_permissions=project_permissions, + ) + + +def ensure_permission(user: UserContext, permission: str) -> None: + if user.is_superadmin: + return + if permission in user.system_permissions: + return + raise web.HTTPForbidden(reason=f"Missing permission: {permission}") + + +async def _get_or_create_service( + request: web.Request, + cache_key: str, + builder: Callable[[web.Request], Awaitable[TService]], +) -> TService: + service = request.get(cache_key) + if service is None: + service = await builder(request) + request[cache_key] = service + return service + + +async def get_validation_service(request: web.Request) -> ValidationService: + async def builder(_: web.Request) -> ValidationService: + pool = await get_pool() + return ValidationService(SchemaRepository(pool)) + + return await _get_or_create_service(request, _VALIDATION_SVC_KEY, builder) + + +async def get_schema_service(request: web.Request) -> SchemaService: + async def builder(_: web.Request) -> SchemaService: + pool = await get_pool() + return SchemaService(SchemaRepository(pool), ConfigRepository(pool)) + + return await _get_or_create_service(request, _SCHEMA_SVC_KEY, builder) + + +async def get_config_service(request: web.Request) -> ConfigService: + async def builder(req: web.Request) -> ConfigService: + pool = await get_pool() + return ConfigService( + ConfigRepository(pool), + HistoryRepository(pool), + await get_validation_service(req), + ) + + return await _get_or_create_service(request, _CONFIG_SVC_KEY, builder) + + +async def get_bulk_service(request: web.Request) -> BulkService: + async def builder(req: web.Request) -> BulkService: + pool = await get_pool() + return BulkService(ConfigRepository(pool), await get_validation_service(req)) + + return await _get_or_create_service(request, _BULK_SVC_KEY, builder) + + +async def get_idempotency_service(request: web.Request) -> IdempotencyService: + async def builder(_: web.Request) -> IdempotencyService: + pool = await get_pool() + return IdempotencyService(IdempotencyRepository(pool)) + + return await _get_or_create_service(request, _IDEMPOTENCY_SVC_KEY, builder) + + +async def get_audit_service(request: web.Request) -> AuditService: + async def builder(_: web.Request) -> AuditService: + return AuditService() + + return await _get_or_create_service(request, _AUDIT_SVC_KEY, builder) diff --git a/projects/backend/services/config-service/src/config_service/services/idempotency.py b/projects/backend/services/config-service/src/config_service/services/idempotency.py new file mode 100644 index 00000000..3ba7cad1 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/idempotency.py @@ -0,0 +1,87 @@ +"""Idempotency service for config-service.""" +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Any + +from aiohttp import web + +from config_service.core.exceptions import IdempotencyConflictError +from config_service.domain.models import IdempotencyRecord +from config_service.repositories.idempotency_repo import IdempotencyRepository +from config_service.settings import settings + +IDEMPOTENCY_HEADER = "Idempotency-Key" + + +@dataclass +class IdempotencyPayload: + status: int + body: dict[str, Any] + + +class IdempotencyService: + def __init__(self, repository: IdempotencyRepository) -> None: + self._repository = repository + + @staticmethod + def body_hash(body: dict[str, Any]) -> str: + serialized = json.dumps(body, sort_keys=True, separators=(",", ":"), default=str) + return hashlib.sha256(serialized.encode()).hexdigest() + + async def get_cached_response( + self, + key: str, + user_id: str, + request_path: str, + body_hash: str, + ) -> IdempotencyPayload | None: + record = await self._repository.get(key) + if record is None: + return None + # Different user — treat as independent (no conflict, no cache) + if record.user_id != user_id: + return None + self._assert_record(record, user_id, request_path, body_hash) + return IdempotencyPayload(status=record.response_status, body=record.response_body) + + async def store_response( + self, + key: str, + user_id: str, + request_path: str, + body_hash: str, + response_status: int, + response_body: dict[str, Any], + ) -> None: + expires_at = datetime.now(tz=timezone.utc) + timedelta( + minutes=settings.idempotency_ttl_minutes + ) + await self._repository.save( + key=key, + user_id=user_id, + request_path=request_path, + request_hash=body_hash, + response_status=response_status, + response_body=response_body, + expires_at=expires_at, + ) + + @staticmethod + def build_response(payload: IdempotencyPayload) -> web.Response: + return web.json_response(payload.body, status=payload.status) + + @staticmethod + def _assert_record( + record: IdempotencyRecord, + user_id: str, + request_path: str, + body_hash: str, + ) -> None: + if record.user_id != user_id or record.request_path != request_path: + raise IdempotencyConflictError(record.idempotency_key) + if record.request_hash != body_hash: + raise IdempotencyConflictError(record.idempotency_key) diff --git a/projects/backend/services/config-service/src/config_service/services/schema_service.py b/projects/backend/services/config-service/src/config_service/services/schema_service.py new file mode 100644 index 00000000..35a0d2f5 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/schema_service.py @@ -0,0 +1,142 @@ +"""Schema management with additive-only compat checker.""" +from __future__ import annotations + +from typing import Any + +import structlog + +from config_service.core.exceptions import ( + SchemaBreakingChangeError, + SchemaSanityFailedError, +) +from config_service.domain.enums import ConfigType +from config_service.domain.models import ConfigSchema +from config_service.prometheus_metrics import ( + config_compat_check_rejections_total, + config_sanity_check_failures_total, +) +from config_service.repositories.config_repo import ConfigRepository +from config_service.repositories.schema_repo import SchemaRepository + +logger = structlog.get_logger(__name__) + +# Keys whose tightening is a breaking change +_NUMERIC_BOUNDS = { + "maximum": lambda old, new: new < old, # lowering maximum is breaking + "minimum": lambda old, new: new > old, # raising minimum is breaking + "maxLength": lambda old, new: new < old, + "minLength": lambda old, new: new > old, +} + + +def _check_compat(old_schema: dict[str, Any], new_schema: dict[str, Any]) -> list[str]: + """Return list of breaking change descriptions (empty = all additive).""" + violations: list[str] = [] + + old_required = set(old_schema.get("required", [])) + new_required = set(new_schema.get("required", [])) + added_required = new_required - old_required + if added_required: + violations.append(f"New required fields added: {sorted(added_required)}") + + old_props = old_schema.get("properties", {}) + new_props = new_schema.get("properties", {}) + + for field in old_props: + if field not in new_props: + violations.append(f"Field removed: '{field}'") + continue + old_f = old_props[field] + new_f = new_props[field] + + old_type = old_f.get("type") + new_type = new_f.get("type") + if old_type and new_type and old_type != new_type: + violations.append(f"Field '{field}' type changed: {old_type} -> {new_type}") + + for bound_key, is_breaking in _NUMERIC_BOUNDS.items(): + old_val = old_f.get(bound_key) + new_val = new_f.get(bound_key) + if old_val is not None and new_val is not None and is_breaking(old_val, new_val): # type: ignore[no-untyped-call] + violations.append( + f"Field '{field}' {bound_key} tightened: {old_val} -> {new_val}" + ) + + old_enum = old_f.get("enum") + new_enum = new_f.get("enum") + if old_enum is not None and new_enum is not None: + removed = set(old_enum) - set(new_enum) + if removed: + violations.append(f"Field '{field}' enum values removed: {sorted(removed)}") + + return violations + + +class SchemaService: + def __init__( + self, + schema_repo: SchemaRepository, + config_repo: ConfigRepository, + ) -> None: + self._schema_repo = schema_repo + self._config_repo = config_repo + + async def get_active(self, config_type: ConfigType) -> ConfigSchema | None: + return await self._schema_repo.get_active(config_type) + + async def list_active(self) -> list[ConfigSchema]: + return await self._schema_repo.list_active() + + async def list_history(self, config_type: ConfigType) -> list[ConfigSchema]: + return await self._schema_repo.list_history(config_type) + + async def update( + self, + config_type: ConfigType, + new_schema: dict[str, Any], + created_by: str, + ) -> ConfigSchema: + current = await self._schema_repo.get_active(config_type) + + if current is not None: + violations = _check_compat(current.schema, new_schema) + if violations: + for v in violations: + config_compat_check_rejections_total.labels( + config_type=config_type.value, + rule=v[:64], + ).inc() + raise SchemaBreakingChangeError(violations) + + import jsonschema + import jsonschema.validators + + validator_cls = jsonschema.validators.validator_for(new_schema) # type: ignore[no-untyped-call, unused-ignore] + validator = validator_cls(new_schema) + + existing_configs = await self._config_repo.list_by_type(config_type) + failures: list[dict[str, Any]] = [] + for cfg in existing_configs: + errs = [e.message for e in validator.iter_errors(cfg.value)] + if errs: + failures.append({"config_id": str(cfg.id), "key": cfg.key, "errors": errs}) + + if failures: + config_sanity_check_failures_total.labels(config_type=config_type.value).inc() + logger.error( + "schema_sanity_check_failed", + config_type=config_type.value, + failures=failures, + ) + raise SchemaSanityFailedError(failures) + + result = await self._schema_repo.insert_new_version_and_activate( + config_type, new_schema, created_by + ) + logger.info( + "schema_updated", + config_type=config_type.value, + version=result.version, + updated_by=created_by, + ) + return result diff --git a/projects/backend/services/config-service/src/config_service/services/validation_service.py b/projects/backend/services/config-service/src/config_service/services/validation_service.py new file mode 100644 index 00000000..a16ed8d0 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/services/validation_service.py @@ -0,0 +1,73 @@ +"""JSON Schema validation for config values.""" +from __future__ import annotations + +from typing import Any + +import jsonschema +import jsonschema.validators +import structlog + +from config_service.core.exceptions import ConfigValidationError, SchemaNotFoundError +from config_service.domain.enums import ConfigType +from config_service.prometheus_metrics import config_read_schema_violations_total +from config_service.repositories.schema_repo import SchemaRepository + +logger = structlog.get_logger(__name__) + + +class ValidationService: + def __init__(self, schema_repo: SchemaRepository) -> None: + self._schema_repo = schema_repo + # Cache: config_type -> compiled validator + self._validators: dict[str, jsonschema.protocols.Validator] = {} + + async def validate_strict(self, config_type: ConfigType, value: dict[str, Any]) -> None: + """Validate on write — raises ConfigValidationError on failure.""" + validator = await self._get_validator(config_type) + errors = [e.message for e in validator.iter_errors(value)] + if errors: + raise ConfigValidationError(errors) + + async def validate_paranoid( + self, + config_type: ConfigType, + value: dict[str, Any], + config_id: str, + ) -> None: + """Validate on read — logs metric on failure but does not raise.""" + try: + validator = await self._get_validator(config_type) + except SchemaNotFoundError: + return + errors = [e.message for e in validator.iter_errors(value)] + if errors: + config_read_schema_violations_total.labels( + config_type=config_type.value, + config_id=config_id, + ).inc() + logger.warning( + "paranoid_schema_violation", + config_id=config_id, + config_type=config_type.value, + errors=errors, + ) + + def invalidate_cache(self, config_type: ConfigType) -> None: + self._validators.pop(config_type.value, None) + + async def _get_validator( + self, config_type: ConfigType + ) -> jsonschema.protocols.Validator: + cached = self._validators.get(config_type.value) + if cached is not None: + return cached + + schema_obj = await self._schema_repo.get_active(config_type) + if schema_obj is None: + raise SchemaNotFoundError(config_type.value) + + validator_cls = jsonschema.validators.validator_for(schema_obj.schema) + validator_cls.check_schema(schema_obj.schema) + validator = validator_cls(schema_obj.schema) + self._validators[config_type.value] = validator + return validator diff --git a/projects/backend/services/config-service/src/config_service/settings.py b/projects/backend/services/config-service/src/config_service/settings.py new file mode 100644 index 00000000..d76878f4 --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/settings.py @@ -0,0 +1,41 @@ +"""Application settings.""" +from __future__ import annotations + +from functools import lru_cache +from typing import cast + +from pydantic import AnyHttpUrl, Field, PostgresDsn + +from backend_common.settings.base import BaseServiceSettings + + +class Settings(BaseServiceSettings): + """Core configuration for the Config Service.""" + + app_name: str = "config-service" + port: int = 8005 + + database_url: PostgresDsn = Field( + default=cast(PostgresDsn, "postgresql://config_user:config_password@localhost:5433/config_db") + ) + + auth_service_url: AnyHttpUrl = Field( + default=cast(AnyHttpUrl, "http://localhost:8001/api/v1") + ) + + otel_exporter_endpoint: AnyHttpUrl | None = None + + # Idempotency key TTL + idempotency_ttl_minutes: int = 15 + + # Background worker cleanup interval + worker_interval_seconds: float = 60.0 + + +@lru_cache +def get_settings() -> Settings: + """Cached settings instance.""" + return Settings() + + +settings = get_settings() diff --git a/projects/backend/services/config-service/src/config_service/workers/__init__.py b/projects/backend/services/config-service/src/config_service/workers/__init__.py new file mode 100644 index 00000000..723db79f --- /dev/null +++ b/projects/backend/services/config-service/src/config_service/workers/__init__.py @@ -0,0 +1,46 @@ +"""Background worker: cleanup expired idempotency keys.""" +from __future__ import annotations + +import asyncio + +import structlog +from aiohttp import web + +from backend_common.db.pool import get_pool +from config_service.repositories.idempotency_repo import IdempotencyRepository +from config_service.settings import settings + +logger = structlog.get_logger(__name__) + +_worker_task: asyncio.Task[None] | None = None + + +async def _cleanup_loop(_app: web.Application) -> None: + while True: + await asyncio.sleep(settings.worker_interval_seconds) + try: + pool = await get_pool() + repo = IdempotencyRepository(pool) + deleted = await repo.delete_expired() + if deleted: + logger.info("idempotency_keys_cleaned_up", count=deleted) + except Exception: + logger.exception("idempotency_cleanup_failed") + + +async def start_background_worker(app: web.Application) -> None: + global _worker_task + _worker_task = asyncio.create_task(_cleanup_loop(app)) + logger.info("background_worker_started") + + +async def stop_background_worker(_app: web.Application) -> None: + global _worker_task + if _worker_task is not None: + _worker_task.cancel() + try: + await _worker_task + except asyncio.CancelledError: + pass + _worker_task = None + logger.info("background_worker_stopped") diff --git a/projects/backend/services/config-service/tests/conftest.py b/projects/backend/services/config-service/tests/conftest.py new file mode 100644 index 00000000..7636a752 --- /dev/null +++ b/projects/backend/services/config-service/tests/conftest.py @@ -0,0 +1,107 @@ +import asyncio +import json +from pathlib import Path + +import asyncpg +import pytest +from testsuite.databases.pgsql import discover, service as pgsql_service + +from config_service.main import create_app +from config_service.settings import settings + +pytest_plugins = ( + "testsuite.pytest_plugin", + "testsuite.databases.pgsql.pytest_plugin", +) + +_PGSQL_CONFIG_DIR = Path(__file__).parent / "pgsql_config" +PG_SCHEMAS_PATH = Path(__file__).parent / "schemas" / "postgresql" + +_FEATURE_FLAG_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": {"type": "boolean"}, + }, + "additionalProperties": False, +} + +_QOS_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["__default__"], + "additionalProperties": {"$ref": "#/$defs/qosSettings"}, + "properties": { + "__default__": {"$ref": "#/$defs/qosSettings"}, + }, + "$defs": { + "qosSettings": { + "type": "object", + "required": ["timeout_ms", "retries"], + "properties": { + "timeout_ms": {"type": "integer", "minimum": 1, "maximum": 600000}, + "retries": {"type": "integer", "minimum": 0, "maximum": 10}, + }, + "additionalProperties": False, + } + }, +} + + +def pytest_service_register(register_service): + def create_pgsql_service(service_name, working_dir, settings=None, env=None): + return pgsql_service.create_pgsql_service( + service_name, + working_dir, + settings=settings, + env={**(env or {}), "POSTGRESQL_CONFIGS_DIR": str(_PGSQL_CONFIG_DIR)}, + ) + + register_service("postgresql", create_pgsql_service) + + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +@pytest.fixture(scope="session") +def pgsql_local(pgsql_local_create): + databases = discover.find_schemas( + service_name=None, + schema_dirs=[PG_SCHEMAS_PATH], + ) + return pgsql_local_create(list(databases.values())) + + +@pytest.fixture +async def service_client(aiohttp_client, pgsql): + """Test client with seed schemas pre-loaded before each test.""" + conninfo = pgsql["config_service"].conninfo + db_url = conninfo.get_uri() + settings.database_url = db_url # type: ignore[assignment] + + # Re-insert seed schemas since testsuite truncates tables between tests + conn = await asyncpg.connect(db_url) + try: + for config_type, schema in [ + ("feature_flag", _FEATURE_FLAG_SCHEMA), + ("qos", _QOS_SCHEMA), + ]: + await conn.execute( + """ + INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) + VALUES ($1, $2::jsonb, 1, true, 'system') + ON CONFLICT DO NOTHING + """, + config_type, + json.dumps(schema), + ) + finally: + await conn.close() + + app = create_app() + return await aiohttp_client(app) diff --git a/projects/backend/services/config-service/tests/pgsql_config/pg_hba.conf b/projects/backend/services/config-service/tests/pgsql_config/pg_hba.conf new file mode 100644 index 00000000..8f797b55 --- /dev/null +++ b/projects/backend/services/config-service/tests/pgsql_config/pg_hba.conf @@ -0,0 +1,4 @@ +# TYPE DATABASE USER ADDRESS METHOD +local all all trust +host all all 0.0.0.0/0 trust +host all all ::/0 trust diff --git a/projects/backend/services/config-service/tests/pgsql_config/postgresql.conf b/projects/backend/services/config-service/tests/pgsql_config/postgresql.conf new file mode 100644 index 00000000..888d459a --- /dev/null +++ b/projects/backend/services/config-service/tests/pgsql_config/postgresql.conf @@ -0,0 +1,22 @@ +listen_addresses = '' + +fsync = off +synchronous_commit = off +full_page_writes = off +autovacuum = off + +log_destination = 'stderr' +logging_collector = on +log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' +log_file_mode = 0666 +client_min_messages = notice +log_min_messages = warning +log_min_error_statement = error +log_min_duration_statement = -1 +log_line_prefix = '%t [%p-%l] %q%u@%d ' +log_lock_waits = on +log_timezone = 'UTC' + +datestyle = 'iso, mdy' +timezone = 'UTC' +default_text_search_config = 'pg_catalog.english' diff --git a/projects/backend/services/config-service/tests/schemas/postgresql/config_service.sql b/projects/backend/services/config-service/tests/schemas/postgresql/config_service.sql new file mode 100644 index 00000000..074828fa --- /dev/null +++ b/projects/backend/services/config-service/tests/schemas/postgresql/config_service.sql @@ -0,0 +1,153 @@ +-- Auto-generated from migrations. +-- Run `poetry run python bin/export_schema.py` after editing migrations. + +BEGIN; + +CREATE TABLE IF NOT EXISTS configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + service_name VARCHAR(128) NOT NULL, + project_id VARCHAR(128), + key VARCHAR(128) NOT NULL, + config_type VARCHAR(32) NOT NULL, + description TEXT, + value JSONB NOT NULL DEFAULT '{}', + metadata JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT true, + is_critical BOOLEAN NOT NULL DEFAULT false, + is_sensitive BOOLEAN NOT NULL DEFAULT false, + version INTEGER NOT NULL DEFAULT 1, + created_by VARCHAR(255) NOT NULL, + updated_by VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_configs_unique_active + ON configs (service_name, COALESCE(project_id, ''), key) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_configs_active + ON configs (service_name, project_id, is_active) + WHERE is_active = true AND deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_configs_service + ON configs (service_name) + WHERE deleted_at IS NULL; + +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS configs_updated_at ON configs; +CREATE TRIGGER configs_updated_at + BEFORE UPDATE ON configs + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TABLE IF NOT EXISTS config_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_id UUID NOT NULL REFERENCES configs(id), + version INTEGER NOT NULL, + service_name VARCHAR(128) NOT NULL, + key VARCHAR(128) NOT NULL, + config_type VARCHAR(32) NOT NULL, + value JSONB NOT NULL DEFAULT '{}', + metadata JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL, + changed_by VARCHAR(255) NOT NULL, + change_reason TEXT, + source_ip VARCHAR(45), + user_agent TEXT, + correlation_id VARCHAR(128), + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_config_history_config_id + ON config_history (config_id, version DESC); + +CREATE TABLE IF NOT EXISTS config_schemas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_type VARCHAR(32) NOT NULL, + schema JSONB NOT NULL, + version INTEGER NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + created_by VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_config_schemas_active_unique + ON config_schemas (config_type) + WHERE is_active = true; + +CREATE INDEX IF NOT EXISTS idx_config_schemas_type + ON config_schemas (config_type, version DESC); + +CREATE TABLE IF NOT EXISTS idempotency_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + idempotency_key VARCHAR(255) NOT NULL UNIQUE, + user_id VARCHAR(255) NOT NULL, + request_path TEXT NOT NULL, + request_hash VARCHAR(64) NOT NULL, + response_status INTEGER NOT NULL, + response_body JSONB NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_idempotency_keys_expires_at + ON idempotency_keys (expires_at); + +CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + checksum TEXT NOT NULL, + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Seed initial schemas +INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) +VALUES ( + 'feature_flag', + '{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" } + }, + "additionalProperties": false + }'::jsonb, + 1, true, 'system' +) ON CONFLICT DO NOTHING; + +INSERT INTO config_schemas (config_type, schema, version, is_active, created_by) +VALUES ( + 'qos', + '{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["__default__"], + "additionalProperties": { "$ref": "#/$defs/qosSettings" }, + "properties": { + "__default__": { "$ref": "#/$defs/qosSettings" } + }, + "$defs": { + "qosSettings": { + "type": "object", + "required": ["timeout_ms", "retries"], + "properties": { + "timeout_ms": { "type": "integer", "minimum": 1, "maximum": 600000 }, + "retries": { "type": "integer", "minimum": 0, "maximum": 10 } + }, + "additionalProperties": false + } + } + }'::jsonb, + 1, true, 'system' +) ON CONFLICT DO NOTHING; + +COMMIT; diff --git a/projects/backend/services/config-service/tests/test_api_bulk.py b/projects/backend/services/config-service/tests/test_api_bulk.py new file mode 100644 index 00000000..62aa87fe --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_bulk.py @@ -0,0 +1,91 @@ +"""Integration tests: bulk endpoint.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS + +_SERVICE = "bulk-svc" + + +async def _create_ff(client, key, enabled=True): + resp = await client.post( + "/api/v1/config", + json={ + "service_name": _SERVICE, + "key": key, + "config_type": "feature_flag", + "value": {"enabled": enabled}, + }, + headers=ADMIN_HEADERS, + ) + assert resp.status == 201, await resp.text() + return await resp.json() + + +@pytest.mark.asyncio +async def test_bulk_returns_active_configs(service_client): + await _create_ff(service_client, "flag_a") + await _create_ff(service_client, "flag_b", enabled=False) + + resp = await service_client.get( + f"/api/v1/configs/bulk?service={_SERVICE}" + ) + assert resp.status == 200, await resp.text() + data = await resp.json() + assert "configs" in data + assert "flag_a" in data["configs"] + assert "ETag" in resp.headers + + +@pytest.mark.asyncio +async def test_bulk_requires_service_param(service_client): + resp = await service_client.get("/api/v1/configs/bulk") + assert resp.status == 400 + + +@pytest.mark.asyncio +async def test_bulk_304_on_same_etag(service_client): + await _create_ff(service_client, "flag_etag") + + resp1 = await service_client.get(f"/api/v1/configs/bulk?service={_SERVICE}") + etag = resp1.headers["ETag"] + + resp2 = await service_client.get( + f"/api/v1/configs/bulk?service={_SERVICE}", + headers={"If-None-Match": etag}, + ) + assert resp2.status == 304 + assert resp2.headers.get("ETag") == etag + + +@pytest.mark.asyncio +async def test_bulk_200_after_update(service_client): + data = await _create_ff(service_client, "flag_change") + config_id = data["id"] + + resp1 = await service_client.get(f"/api/v1/configs/bulk?service={_SERVICE}") + etag1 = resp1.headers["ETag"] + + # Patch the config + await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "test"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + resp2 = await service_client.get( + f"/api/v1/configs/bulk?service={_SERVICE}", + headers={"If-None-Match": etag1}, + ) + assert resp2.status == 200 + etag2 = resp2.headers["ETag"] + assert etag2 != etag1 + + +@pytest.mark.asyncio +async def test_bulk_empty_service(service_client): + resp = await service_client.get("/api/v1/configs/bulk?service=nonexistent-svc-xyz") + assert resp.status == 200 + data = await resp.json() + assert data["configs"] == {} diff --git a/projects/backend/services/config-service/tests/test_api_configs.py b/projects/backend/services/config-service/tests/test_api_configs.py new file mode 100644 index 00000000..1ce71adc --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_configs.py @@ -0,0 +1,205 @@ +"""Integration tests: config CRUD lifecycle.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS, EDITOR_HEADERS, VIEWER_HEADERS, make_headers + +_BASE_PAYLOAD = { + "service_name": "auth-service", + "key": "write_path_enabled", + "config_type": "feature_flag", + "value": {"enabled": True}, +} + + +async def _create(client, payload=None, headers=None, idempotency_key=None): + payload = payload or _BASE_PAYLOAD + hdrs = {**(headers or ADMIN_HEADERS)} + if idempotency_key: + hdrs["Idempotency-Key"] = idempotency_key + resp = await client.post("/api/v1/config", json=payload, headers=hdrs) + return resp + + +@pytest.mark.asyncio +async def test_create_and_get(service_client): + resp = await _create(service_client) + assert resp.status == 201, await resp.text() + data = await resp.json() + assert data["key"] == "write_path_enabled" + assert data["version"] == 1 + assert resp.headers.get("ETag") == '"1"' + + config_id = data["id"] + get_resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS + ) + assert get_resp.status == 200 + got = await get_resp.json() + assert got["id"] == config_id + assert got["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_list_configs(service_client): + await _create(service_client) + resp = await service_client.get( + "/api/v1/config?service=auth-service", headers=ADMIN_HEADERS + ) + assert resp.status == 200 + data = await resp.json() + assert len(data["items"]) >= 1 + + +@pytest.mark.asyncio +async def test_patch_config(service_client): + resp = await _create(service_client) + data = await resp.json() + config_id = data["id"] + + patch_resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "INC-001"}, + headers={**EDITOR_HEADERS, "If-Match": '"1"'}, + ) + assert patch_resp.status == 200, await patch_resp.text() + patched = await patch_resp.json() + assert patched["version"] == 2 + assert patched["value"] == {"enabled": False} + assert patch_resp.headers.get("ETag") == '"2"' + + +@pytest.mark.asyncio +async def test_patch_increments_version(service_client): + resp = await _create(service_client) + config_id = (await resp.json())["id"] + + for i in range(1, 4): + patch_resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={ + "version": i, + "value": {"enabled": i % 2 == 0}, + "change_reason": f"step-{i}", + }, + headers={**ADMIN_HEADERS, "If-Match": f'"{i}"'}, + ) + assert patch_resp.status == 200 + assert (await patch_resp.json())["version"] == i + 1 + + +@pytest.mark.asyncio +async def test_soft_delete(service_client): + resp = await _create(service_client) + config_id = (await resp.json())["id"] + + del_resp = await service_client.delete( + f"/api/v1/config/{config_id}?version=1&change_reason=cleanup", + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert del_resp.status == 204 + + get_resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS + ) + assert get_resp.status == 404 + + +@pytest.mark.asyncio +async def test_recreate_after_soft_delete(service_client): + """Same (service, key) can be created after soft-delete.""" + resp = await _create(service_client) + config_id = (await resp.json())["id"] + + await service_client.delete( + f"/api/v1/config/{config_id}?version=1&change_reason=cleanup", + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + resp2 = await _create(service_client) + assert resp2.status == 201 + data2 = await resp2.json() + assert data2["id"] != config_id + assert data2["version"] == 1 + + +@pytest.mark.asyncio +async def test_get_nonexistent(service_client): + resp = await service_client.get( + "/api/v1/config/00000000-0000-0000-0000-000000000000", + headers=ADMIN_HEADERS, + ) + assert resp.status == 404 + + +@pytest.mark.asyncio +async def test_create_missing_user(service_client): + resp = await service_client.post("/api/v1/config", json=_BASE_PAYLOAD) + assert resp.status == 401 + + +@pytest.mark.asyncio +async def test_create_invalid_value(service_client): + """Value that doesn't match feature_flag schema → 422.""" + payload = {**_BASE_PAYLOAD, "value": {"enabled": "not-a-bool"}} + resp = await _create(service_client, payload=payload) + assert resp.status == 422 + + +@pytest.mark.asyncio +async def test_delete_missing_change_reason(service_client): + resp = await _create(service_client) + config_id = (await resp.json())["id"] + del_resp = await service_client.delete( + f"/api/v1/config/{config_id}?version=1", + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert del_resp.status == 400 + + +@pytest.mark.asyncio +async def test_activate_deactivate(service_client): + resp = await _create(service_client) + data = await resp.json() + config_id = data["id"] + + # Deactivate + deact = await service_client.post( + f"/api/v1/config/{config_id}/deactivate", + json={"version": 1, "change_reason": "maintenance"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert deact.status == 200 + assert (await deact.json())["is_active"] is False + + # Activate + act = await service_client.post( + f"/api/v1/config/{config_id}/activate", + json={"version": 2, "change_reason": "done"}, + headers={**ADMIN_HEADERS, "If-Match": '"2"'}, + ) + assert act.status == 200 + assert (await act.json())["is_active"] is True + + +@pytest.mark.asyncio +async def test_get_history(service_client): + resp = await _create(service_client) + config_id = (await resp.json())["id"] + + await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "v2"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + hist_resp = await service_client.get( + f"/api/v1/config/{config_id}/history", headers=ADMIN_HEADERS + ) + assert hist_resp.status == 200 + items = (await hist_resp.json())["items"] + assert len(items) == 2 + # History is sorted newest-first (ORDER BY version DESC) + assert items[0]["version"] == 2 + assert items[1]["version"] == 1 diff --git a/projects/backend/services/config-service/tests/test_api_rbac.py b/projects/backend/services/config-service/tests/test_api_rbac.py new file mode 100644 index 00000000..9cf268bc --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_rbac.py @@ -0,0 +1,121 @@ +"""Integration tests: RBAC permission matrix.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS, EDITOR_HEADERS, VIEWER_HEADERS, make_headers + +_PAYLOAD = { + "service_name": "rbac-svc", + "key": "rbac_flag", + "config_type": "feature_flag", + "value": {"enabled": True}, +} + + +async def _create(client, headers=None): + resp = await client.post( + "/api/v1/config", + json={**_PAYLOAD, "key": f"rbac_{id(client)}_flag"}, + headers=headers or ADMIN_HEADERS, + ) + return resp + + +@pytest.mark.asyncio +async def test_viewer_can_list(service_client): + resp = await service_client.get( + "/api/v1/config?service=rbac-svc", headers=VIEWER_HEADERS + ) + assert resp.status == 200 + + +@pytest.mark.asyncio +async def test_viewer_cannot_create(service_client): + resp = await service_client.post( + "/api/v1/config", json=_PAYLOAD, headers=VIEWER_HEADERS + ) + assert resp.status == 403 + + +@pytest.mark.asyncio +async def test_viewer_cannot_patch(service_client): + create_resp = await _create(service_client) + config_id = (await create_resp.json())["id"] + + resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "test"}, + headers={**VIEWER_HEADERS, "If-Match": '"1"'}, + ) + assert resp.status == 403 + + +@pytest.mark.asyncio +async def test_viewer_cannot_delete(service_client): + create_resp = await _create(service_client) + config_id = (await create_resp.json())["id"] + + resp = await service_client.delete( + f"/api/v1/config/{config_id}?version=1&change_reason=cleanup", + headers={**VIEWER_HEADERS, "If-Match": '"1"'}, + ) + assert resp.status == 403 + + +@pytest.mark.asyncio +async def test_editor_can_create(service_client): + resp = await service_client.post( + "/api/v1/config", + json={**_PAYLOAD, "key": "editor_created_flag"}, + headers=EDITOR_HEADERS, + ) + assert resp.status == 201 + + +@pytest.mark.asyncio +async def test_editor_can_patch(service_client): + create_resp = await _create(service_client) + config_id = (await create_resp.json())["id"] + + resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "edit"}, + headers={**EDITOR_HEADERS, "If-Match": '"1"'}, + ) + assert resp.status == 200 + + +@pytest.mark.asyncio +async def test_editor_cannot_manage_schemas(service_client): + current = await ( + await service_client.get("/api/v1/schemas/feature_flag", headers=ADMIN_HEADERS) + ).json() + + resp = await service_client.put( + "/api/v1/schemas/feature_flag", + json={"schema": current["schema"]}, + headers=EDITOR_HEADERS, + ) + assert resp.status == 403 + + +@pytest.mark.asyncio +async def test_unauthenticated_returns_401(service_client): + for endpoint, method in [ + ("/api/v1/config", "get"), + ("/api/v1/schemas", "get"), + ]: + resp = await getattr(service_client, method)(endpoint) + assert resp.status == 401, f"{method.upper()} {endpoint} should be 401" + + +@pytest.mark.asyncio +async def test_superadmin_has_full_access(service_client): + superadmin = make_headers(user_id="super", is_superadmin=True) + resp = await service_client.post( + "/api/v1/config", + json={**_PAYLOAD, "key": "superadmin_flag"}, + headers=superadmin, + ) + assert resp.status == 201 diff --git a/projects/backend/services/config-service/tests/test_api_rollback.py b/projects/backend/services/config-service/tests/test_api_rollback.py new file mode 100644 index 00000000..6f9ddf89 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_rollback.py @@ -0,0 +1,120 @@ +"""Integration tests: rollback endpoint.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS + + +async def _create_ff(client, key="ff_key"): + resp = await client.post( + "/api/v1/config", + json={ + "service_name": "exp-service", + "key": key, + "config_type": "feature_flag", + "value": {"enabled": True}, + }, + headers=ADMIN_HEADERS, + ) + assert resp.status == 201, await resp.text() + return await resp.json() + + +async def _patch(client, config_id, version, value, reason="change"): + resp = await client.patch( + f"/api/v1/config/{config_id}", + json={"version": version, "value": value, "change_reason": reason}, + headers={**ADMIN_HEADERS, "If-Match": f'"{version}"'}, + ) + assert resp.status == 200, await resp.text() + return await resp.json() + + +@pytest.mark.asyncio +async def test_rollback_to_v1(service_client): + data = await _create_ff(service_client) + config_id = data["id"] + + # v2 + await _patch(service_client, config_id, 1, {"enabled": False}) + # v3 + await _patch(service_client, config_id, 2, {"enabled": True}) + + # Rollback v3 → v1 + rb = await service_client.post( + f"/api/v1/config/{config_id}/rollback", + json={"version": 3, "target_version": 1, "change_reason": "restore v1"}, + headers={**ADMIN_HEADERS, "If-Match": '"3"'}, + ) + assert rb.status == 200, await rb.text() + result = await rb.json() + assert result["version"] == 4 + assert result["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_rollback_produces_history_entry(service_client): + data = await _create_ff(service_client, key="rb_hist_key") + config_id = data["id"] + + await _patch(service_client, config_id, 1, {"enabled": False}) + + await service_client.post( + f"/api/v1/config/{config_id}/rollback", + json={"version": 2, "target_version": 1, "change_reason": "revert"}, + headers={**ADMIN_HEADERS, "If-Match": '"2"'}, + ) + + hist = await service_client.get( + f"/api/v1/config/{config_id}/history", headers=ADMIN_HEADERS + ) + items = (await hist.json())["items"] + assert len(items) == 3 + # History is sorted newest-first (ORDER BY version DESC) + assert items[0]["change_reason"] == "revert" + assert items[0]["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_rollback_to_nonexistent_version(service_client): + data = await _create_ff(service_client, key="rb_noexist") + config_id = data["id"] + + rb = await service_client.post( + f"/api/v1/config/{config_id}/rollback", + json={"version": 1, "target_version": 99, "change_reason": "bad"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert rb.status == 404 + + +@pytest.mark.asyncio +async def test_rollback_same_version_rejected(service_client): + """target_version == version is invalid per RollbackRequest validator.""" + data = await _create_ff(service_client, key="rb_same_v") + config_id = data["id"] + + rb = await service_client.post( + f"/api/v1/config/{config_id}/rollback", + json={"version": 1, "target_version": 1, "change_reason": "bad"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert rb.status == 422 + + +@pytest.mark.asyncio +async def test_rollback_chain(service_client): + """v1 → v2 → rollback→v1 becomes v3, then rollback→v2 becomes v4.""" + data = await _create_ff(service_client, key="rb_chain") + config_id = data["id"] + + await _patch(service_client, config_id, 1, {"enabled": False}, "v2") + + rb1 = await service_client.post( + f"/api/v1/config/{config_id}/rollback", + json={"version": 2, "target_version": 1, "change_reason": "back to v1"}, + headers={**ADMIN_HEADERS, "If-Match": '"2"'}, + ) + assert (await rb1.json())["version"] == 3 + assert (await service_client.get(f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS)).status == 200 diff --git a/projects/backend/services/config-service/tests/test_api_schemas.py b/projects/backend/services/config-service/tests/test_api_schemas.py new file mode 100644 index 00000000..6eb44359 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_schemas.py @@ -0,0 +1,117 @@ +"""Integration tests: schema management endpoints.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS, VIEWER_HEADERS, make_headers + +_SCHEMA_ADMIN_HEADERS = make_headers( + user_id="schema-admin", + system_permissions=["configs.schemas.manage"], +) + + +@pytest.mark.asyncio +async def test_list_schemas(service_client): + resp = await service_client.get("/api/v1/schemas", headers=ADMIN_HEADERS) + assert resp.status == 200 + data = await resp.json() + # Seed data from 002_seed_schemas.sql provides at least feature_flag + qos + types = [s["config_type"] for s in data["items"]] + assert "feature_flag" in types + assert "qos" in types + + +@pytest.mark.asyncio +async def test_get_schema_feature_flag(service_client): + resp = await service_client.get( + "/api/v1/schemas/feature_flag", headers=ADMIN_HEADERS + ) + assert resp.status == 200 + data = await resp.json() + assert data["config_type"] == "feature_flag" + assert data["is_active"] is True + assert "properties" in data["schema"] + + +@pytest.mark.asyncio +async def test_get_schema_not_found(service_client): + resp = await service_client.get( + "/api/v1/schemas/unknown_type", headers=ADMIN_HEADERS + ) + assert resp.status == 400 # invalid config_type + + +@pytest.mark.asyncio +async def test_get_schema_history(service_client): + resp = await service_client.get( + "/api/v1/schemas/feature_flag/history", headers=ADMIN_HEADERS + ) + assert resp.status == 200 + items = (await resp.json())["items"] + assert len(items) >= 1 + + +@pytest.mark.asyncio +async def test_update_schema_additive(service_client): + """Adding an optional field is additive → allowed.""" + # Get current schema first + current = await ( + await service_client.get("/api/v1/schemas/feature_flag", headers=ADMIN_HEADERS) + ).json() + + new_schema = { + **current["schema"], + "properties": { + **current["schema"]["properties"], + "rollout_percentage": {"type": "integer", "minimum": 0, "maximum": 100}, + }, + } + + resp = await service_client.put( + "/api/v1/schemas/feature_flag", + json={"schema": new_schema}, + headers={**ADMIN_HEADERS, "X-User-System-Permissions": "configs.schemas.manage"}, + ) + assert resp.status == 200, await resp.text() + data = await resp.json() + assert data["version"] == current["version"] + 1 + + +@pytest.mark.asyncio +async def test_update_schema_breaking_rejected(service_client): + """Removing a field is a breaking change → 422.""" + current = await ( + await service_client.get("/api/v1/schemas/feature_flag", headers=ADMIN_HEADERS) + ).json() + + props = {**current["schema"]["properties"]} + props.pop("enabled", None) + breaking_schema = {**current["schema"], "properties": props} + + resp = await service_client.put( + "/api/v1/schemas/feature_flag", + json={"schema": breaking_schema}, + headers={**ADMIN_HEADERS, "X-User-System-Permissions": "configs.schemas.manage"}, + ) + assert resp.status == 422 + + +@pytest.mark.asyncio +async def test_update_schema_requires_permission(service_client): + current = await ( + await service_client.get("/api/v1/schemas/feature_flag", headers=ADMIN_HEADERS) + ).json() + + resp = await service_client.put( + "/api/v1/schemas/feature_flag", + json={"schema": current["schema"]}, + headers=VIEWER_HEADERS, # no manage permission + ) + assert resp.status == 403 + + +@pytest.mark.asyncio +async def test_list_schemas_requires_auth(service_client): + resp = await service_client.get("/api/v1/schemas") + assert resp.status == 401 diff --git a/projects/backend/services/config-service/tests/test_api_sensitive.py b/projects/backend/services/config-service/tests/test_api_sensitive.py new file mode 100644 index 00000000..0898a5c1 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_api_sensitive.py @@ -0,0 +1,96 @@ +"""Integration tests: sensitive config redaction.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS, VIEWER_HEADERS, make_headers + +_SENSITIVE_PAYLOAD = { + "service_name": "sens-svc", + "key": "api_secret", + "config_type": "feature_flag", + "value": {"enabled": True}, + "is_sensitive": True, +} + +_SENSITIVE_READ_HEADERS = make_headers( + user_id="sensitive-reader", + system_permissions=["configs.view", "configs.sensitive.read"], +) + + +@pytest.mark.asyncio +async def test_sensitive_redacted_for_viewer(service_client): + create_resp = await service_client.post( + "/api/v1/config", json=_SENSITIVE_PAYLOAD, headers=ADMIN_HEADERS + ) + config_id = (await create_resp.json())["id"] + + resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=VIEWER_HEADERS + ) + assert resp.status == 200 + data = await resp.json() + assert data["value"] == "***" + + +@pytest.mark.asyncio +async def test_sensitive_visible_to_superadmin(service_client): + create_resp = await service_client.post( + "/api/v1/config", json=_SENSITIVE_PAYLOAD, headers=ADMIN_HEADERS + ) + config_id = (await create_resp.json())["id"] + + resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS + ) + data = await resp.json() + assert data["value"] != "***" + assert data["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_sensitive_visible_with_sensitive_read_permission(service_client): + create_resp = await service_client.post( + "/api/v1/config", json=_SENSITIVE_PAYLOAD, headers=ADMIN_HEADERS + ) + config_id = (await create_resp.json())["id"] + + resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=_SENSITIVE_READ_HEADERS + ) + data = await resp.json() + assert data["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_sensitive_redacted_in_list(service_client): + await service_client.post( + "/api/v1/config", + json={**_SENSITIVE_PAYLOAD, "key": "secret_list_flag"}, + headers=ADMIN_HEADERS, + ) + + resp = await service_client.get( + "/api/v1/config?service=sens-svc", headers=VIEWER_HEADERS + ) + data = await resp.json() + sensitive_items = [i for i in data["items"] if i.get("is_sensitive")] + for item in sensitive_items: + assert item["value"] == "***" + + +@pytest.mark.asyncio +async def test_non_sensitive_not_redacted_for_viewer(service_client): + create_resp = await service_client.post( + "/api/v1/config", + json={**_SENSITIVE_PAYLOAD, "key": "public_flag", "is_sensitive": False}, + headers=ADMIN_HEADERS, + ) + config_id = (await create_resp.json())["id"] + + resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=VIEWER_HEADERS + ) + data = await resp.json() + assert data["value"] == {"enabled": True} diff --git a/projects/backend/services/config-service/tests/test_domain_models.py b/projects/backend/services/config-service/tests/test_domain_models.py new file mode 100644 index 00000000..8dbf860d --- /dev/null +++ b/projects/backend/services/config-service/tests/test_domain_models.py @@ -0,0 +1,81 @@ +"""Unit tests for domain DTOs and models.""" +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from config_service.domain.dto import ( + ConfigCreate, + ConfigPatch, + RollbackRequest, + SchemaUpdateRequest, +) +from config_service.domain.enums import ConfigType + + +def test_config_create_valid(): + dto = ConfigCreate( + service_name="my-service", + key="write_path_enabled", + config_type=ConfigType.feature_flag, + value={"enabled": True}, + ) + assert dto.service_name == "my-service" + assert dto.config_type == ConfigType.feature_flag + assert dto.metadata == {} + assert dto.is_critical is False + assert dto.is_sensitive is False + + +def test_config_create_missing_required_fields(): + with pytest.raises(ValidationError): + ConfigCreate(key="k", config_type=ConfigType.feature_flag, value={}) # type: ignore + + +def test_config_create_empty_service_name(): + with pytest.raises(ValidationError): + ConfigCreate( + service_name="", + key="k", + config_type=ConfigType.feature_flag, + value={}, + ) + + +def test_config_patch_requires_change_reason(): + with pytest.raises(ValidationError): + ConfigPatch(version=1, change_reason="") + + +def test_config_patch_valid(): + dto = ConfigPatch( + version=1, + change_reason="INC-1234", + value={"enabled": False}, + ) + assert dto.version == 1 + assert dto.change_reason == "INC-1234" + assert dto.value == {"enabled": False} + assert dto.is_active is None + + +def test_rollback_request_target_must_differ(): + with pytest.raises(ValidationError): + RollbackRequest(version=2, target_version=2, change_reason="test") + + +def test_rollback_request_valid(): + dto = RollbackRequest(version=3, target_version=1, change_reason="restore") + assert dto.target_version == 1 + + +def test_schema_update_request_alias(): + dto = SchemaUpdateRequest.model_validate({"schema": {"type": "object"}}) + assert dto.schema_ == {"type": "object"} + + +def test_config_type_enum_values(): + assert ConfigType("feature_flag") == ConfigType.feature_flag + assert ConfigType("qos") == ConfigType.qos + with pytest.raises(ValueError): + ConfigType("unknown") diff --git a/projects/backend/services/config-service/tests/test_dry_run.py b/projects/backend/services/config-service/tests/test_dry_run.py new file mode 100644 index 00000000..dd13b4c3 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_dry_run.py @@ -0,0 +1,98 @@ +"""Integration tests: dry-run mode.""" +from __future__ import annotations + +import pytest + +from tests.utils import ADMIN_HEADERS + +_PAYLOAD = { + "service_name": "dry-svc", + "key": "dry_flag", + "config_type": "feature_flag", + "value": {"enabled": True}, +} + + +@pytest.mark.asyncio +async def test_dry_run_create_returns_preview(service_client): + resp = await service_client.post( + "/api/v1/config?dry_run=true", + json=_PAYLOAD, + headers=ADMIN_HEADERS, + ) + assert resp.status == 200, await resp.text() + data = await resp.json() + assert data["dry_run"] is True + assert "preview" in data + assert data["preview"]["key"] == "dry_flag" + + +@pytest.mark.asyncio +async def test_dry_run_create_no_db_write(service_client): + """After dry-run POST, no config should exist in DB.""" + await service_client.post( + "/api/v1/config?dry_run=true", + json=_PAYLOAD, + headers=ADMIN_HEADERS, + ) + + list_resp = await service_client.get( + "/api/v1/config?service=dry-svc", headers=ADMIN_HEADERS + ) + data = await list_resp.json() + assert len(data["items"]) == 0 + + +@pytest.mark.asyncio +async def test_dry_run_patch_returns_preview(service_client): + create_resp = await service_client.post( + "/api/v1/config", + json=_PAYLOAD, + headers=ADMIN_HEADERS, + ) + config_id = (await create_resp.json())["id"] + + resp = await service_client.patch( + f"/api/v1/config/{config_id}?dry_run=true", + json={"version": 1, "value": {"enabled": False}, "change_reason": "test"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert resp.status == 200 + data = await resp.json() + assert data["dry_run"] is True + assert data["preview"]["value"] == {"enabled": False} + + +@pytest.mark.asyncio +async def test_dry_run_patch_no_db_write(service_client): + create_resp = await service_client.post( + "/api/v1/config", + json={**_PAYLOAD, "key": "dry_patch_stable"}, + headers=ADMIN_HEADERS, + ) + config_id = (await create_resp.json())["id"] + + await service_client.patch( + f"/api/v1/config/{config_id}?dry_run=true", + json={"version": 1, "value": {"enabled": False}, "change_reason": "test"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + # Version should still be 1 + get_resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS + ) + data = await get_resp.json() + assert data["version"] == 1 + assert data["value"] == {"enabled": True} + + +@pytest.mark.asyncio +async def test_dry_run_validation_failure(service_client): + """Dry-run with invalid value returns 422 (no write).""" + resp = await service_client.post( + "/api/v1/config?dry_run=true", + json={**_PAYLOAD, "value": {"enabled": "bad"}}, + headers=ADMIN_HEADERS, + ) + assert resp.status == 422 diff --git a/projects/backend/services/config-service/tests/test_health.py b/projects/backend/services/config-service/tests/test_health.py new file mode 100644 index 00000000..696a83f8 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_health.py @@ -0,0 +1,19 @@ +"""Integration tests: health endpoint.""" +from __future__ import annotations + +import pytest + + +@pytest.mark.asyncio +async def test_health_ok(service_client): + resp = await service_client.get("/health") + assert resp.status == 200 + data = await resp.json() + assert data["status"] == "ok" + + +@pytest.mark.asyncio +async def test_health_no_auth_required(service_client): + """Health endpoint must not require authentication.""" + resp = await service_client.get("/health") + assert resp.status == 200 diff --git a/projects/backend/services/config-service/tests/test_idempotency_integration.py b/projects/backend/services/config-service/tests/test_idempotency_integration.py new file mode 100644 index 00000000..fc0ba48e --- /dev/null +++ b/projects/backend/services/config-service/tests/test_idempotency_integration.py @@ -0,0 +1,90 @@ +"""Integration tests: Idempotency-Key header.""" +from __future__ import annotations + +import uuid + +import pytest + +from tests.utils import ADMIN_HEADERS, make_headers + +_PAYLOAD = { + "service_name": "idem-svc", + "key": "idem_flag", + "config_type": "feature_flag", + "value": {"enabled": True}, +} + + +@pytest.mark.asyncio +async def test_same_key_returns_same_id(service_client): + idem_key = str(uuid.uuid4()) + hdrs = {**ADMIN_HEADERS, "Idempotency-Key": idem_key} + + r1 = await service_client.post("/api/v1/config", json=_PAYLOAD, headers=hdrs) + assert r1.status == 201, await r1.text() + id1 = (await r1.json())["id"] + + r2 = await service_client.post("/api/v1/config", json=_PAYLOAD, headers=hdrs) + assert r2.status == 201 + id2 = (await r2.json())["id"] + + assert id1 == id2 + + +@pytest.mark.asyncio +async def test_different_body_same_key_returns_409(service_client): + idem_key = str(uuid.uuid4()) + hdrs = {**ADMIN_HEADERS, "Idempotency-Key": idem_key} + + r1 = await service_client.post("/api/v1/config", json=_PAYLOAD, headers=hdrs) + assert r1.status == 201 + + different_payload = {**_PAYLOAD, "value": {"enabled": False}} + r2 = await service_client.post("/api/v1/config", json=different_payload, headers=hdrs) + assert r2.status == 409 + + +@pytest.mark.asyncio +async def test_different_user_same_key_creates_new(service_client): + """Same Idempotency-Key from different users: each user's own request is idempotent, + but different users with the same key don't conflict with each other.""" + idem_key = str(uuid.uuid4()) + suffix = uuid.uuid4().hex[:6] + # Each user creates a DIFFERENT config key to avoid the unique (service,key) constraint + payload_a = {**_PAYLOAD, "key": f"idem_user_a_{suffix}"} + payload_b = {**_PAYLOAD, "key": f"idem_user_b_{suffix}"} + + hdrs_a = {**ADMIN_HEADERS, "Idempotency-Key": idem_key} + hdrs_b = { + **make_headers(user_id="other-admin", is_superadmin=True), + "Idempotency-Key": idem_key, + } + + r1 = await service_client.post("/api/v1/config", json=payload_a, headers=hdrs_a) + assert r1.status == 201 + id1 = (await r1.json())["id"] + + r2 = await service_client.post("/api/v1/config", json=payload_b, headers=hdrs_b) + assert r2.status == 201 + id2 = (await r2.json())["id"] + + assert id1 != id2 + + # User A repeating with same key and payload returns same id (idempotent) + r3 = await service_client.post("/api/v1/config", json=payload_a, headers=hdrs_a) + assert r3.status == 201 + assert (await r3.json())["id"] == id1 + + +@pytest.mark.asyncio +async def test_no_idempotency_key_creates_duplicates(service_client): + """Without Idempotency-Key, two POSTs with different keys work fine.""" + p1 = {**_PAYLOAD, "key": f"no_idem_a_{uuid.uuid4().hex[:6]}"} + p2 = {**_PAYLOAD, "key": f"no_idem_b_{uuid.uuid4().hex[:6]}"} + + r1 = await service_client.post("/api/v1/config", json=p1, headers=ADMIN_HEADERS) + r2 = await service_client.post("/api/v1/config", json=p2, headers=ADMIN_HEADERS) + + assert r1.status == 201 + assert r2.status == 201 + assert (await r1.json())["id"] != (await r2.json())["id"] diff --git a/projects/backend/services/config-service/tests/test_idempotency_service.py b/projects/backend/services/config-service/tests/test_idempotency_service.py new file mode 100644 index 00000000..3b7f23ff --- /dev/null +++ b/projects/backend/services/config-service/tests/test_idempotency_service.py @@ -0,0 +1,102 @@ +"""Unit tests for IdempotencyService (mocked repo).""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from unittest.mock import AsyncMock, MagicMock +from uuid import uuid4 + +import pytest + +from config_service.core.exceptions import IdempotencyConflictError +from config_service.domain.models import IdempotencyRecord +from config_service.services.idempotency import IdempotencyService + + +def _make_record(key: str, user_id: str, path: str, body_hash: str) -> IdempotencyRecord: + return IdempotencyRecord( + id=uuid4(), + idempotency_key=key, + user_id=user_id, + request_path=path, + request_hash=body_hash, + response_status=201, + response_body={"id": str(uuid4())}, + expires_at=datetime.now(tz=timezone.utc) + timedelta(minutes=15), + created_at=datetime.now(tz=timezone.utc), + ) + + +def test_body_hash_is_deterministic(): + body = {"service_name": "svc", "key": "k", "value": {"enabled": True}} + h1 = IdempotencyService.body_hash(body) + h2 = IdempotencyService.body_hash(body) + assert h1 == h2 + + +def test_body_hash_differs_for_different_bodies(): + h1 = IdempotencyService.body_hash({"enabled": True}) + h2 = IdempotencyService.body_hash({"enabled": False}) + assert h1 != h2 + + +def test_body_hash_is_order_independent(): + h1 = IdempotencyService.body_hash({"a": 1, "b": 2}) + h2 = IdempotencyService.body_hash({"b": 2, "a": 1}) + assert h1 == h2 + + +@pytest.mark.asyncio +async def test_no_cached_response_returns_none(): + repo = MagicMock() + repo.get = AsyncMock(return_value=None) + svc = IdempotencyService(repo) + result = await svc.get_cached_response("key", "user", "/path", "hash") + assert result is None + + +@pytest.mark.asyncio +async def test_cached_response_same_hash_returns_payload(): + key = "my-key" + user_id = "user-1" + path = "/api/v1/config" + body_hash = "abc123" + + record = _make_record(key, user_id, path, body_hash) + repo = MagicMock() + repo.get = AsyncMock(return_value=record) + svc = IdempotencyService(repo) + + result = await svc.get_cached_response(key, user_id, path, body_hash) + assert result is not None + assert result.status == 201 + + +@pytest.mark.asyncio +async def test_conflict_on_different_body_hash(): + key = "my-key" + user_id = "user-1" + path = "/api/v1/config" + record = _make_record(key, user_id, path, "hash-original") + + repo = MagicMock() + repo.get = AsyncMock(return_value=record) + svc = IdempotencyService(repo) + + with pytest.raises(IdempotencyConflictError): + await svc.get_cached_response(key, user_id, path, "hash-different") + + +@pytest.mark.asyncio +async def test_different_user_same_key_returns_none(): + """Different user with the same idempotency key is treated as independent — no conflict.""" + key = "my-key" + path = "/api/v1/config" + body_hash = "same-hash" + record = _make_record(key, "user-1", path, body_hash) + + repo = MagicMock() + repo.get = AsyncMock(return_value=record) + svc = IdempotencyService(repo) + + result = await svc.get_cached_response(key, "user-2", path, body_hash) + assert result is None diff --git a/projects/backend/services/config-service/tests/test_optimistic_lock.py b/projects/backend/services/config-service/tests/test_optimistic_lock.py new file mode 100644 index 00000000..5aea58ec --- /dev/null +++ b/projects/backend/services/config-service/tests/test_optimistic_lock.py @@ -0,0 +1,111 @@ +"""Integration tests: optimistic locking.""" +from __future__ import annotations + +import asyncio + +import pytest + +from tests.utils import ADMIN_HEADERS, EDITOR_HEADERS + + +async def _create(client): + resp = await client.post( + "/api/v1/config", + json={ + "service_name": "lock-svc", + "key": "lock_key", + "config_type": "feature_flag", + "value": {"enabled": True}, + }, + headers=ADMIN_HEADERS, + ) + assert resp.status == 201, await resp.text() + return await resp.json() + + +@pytest.mark.asyncio +async def test_missing_if_match_returns_428(service_client): + data = await _create(service_client) + config_id = data["id"] + + resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "test"}, + headers=ADMIN_HEADERS, # no If-Match + ) + assert resp.status == 428 + + +@pytest.mark.asyncio +async def test_version_mismatch_between_header_and_body_returns_400(service_client): + data = await _create(service_client) + config_id = data["id"] + + resp = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 2, "value": {"enabled": False}, "change_reason": "test"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, # header=1, body=2 + ) + assert resp.status == 400 + + +@pytest.mark.asyncio +async def test_stale_version_returns_412(service_client): + """Patch with already-consumed version → 412 Precondition Failed.""" + data = await _create(service_client) + config_id = data["id"] + + # First patch succeeds → version becomes 2 + ok = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "first"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert ok.status == 200 + + # Second patch with old version=1 → 412 + conflict = await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": True}, "change_reason": "stale"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert conflict.status == 412 + + +@pytest.mark.asyncio +async def test_concurrent_patches_one_wins(service_client): + """Two concurrent PATCHes with version=1; exactly one should succeed.""" + data = await _create(service_client) + config_id = data["id"] + + async def do_patch(): + return await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "race"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + results = await asyncio.gather(do_patch(), do_patch(), return_exceptions=True) + statuses = [r.status for r in results if hasattr(r, "status")] + assert 200 in statuses + assert 412 in statuses + + +@pytest.mark.asyncio +async def test_delete_stale_version_returns_412(service_client): + data = await _create(service_client) + config_id = data["id"] + + # Move version to 2 + await service_client.patch( + f"/api/v1/config/{config_id}", + json={"version": 1, "value": {"enabled": False}, "change_reason": "bump"}, + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + + # Try delete with old version + resp = await service_client.delete( + f"/api/v1/config/{config_id}?version=1&change_reason=cleanup", + headers={**ADMIN_HEADERS, "If-Match": '"1"'}, + ) + assert resp.status == 412 diff --git a/projects/backend/services/config-service/tests/test_paranoid_read.py b/projects/backend/services/config-service/tests/test_paranoid_read.py new file mode 100644 index 00000000..f6a76b86 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_paranoid_read.py @@ -0,0 +1,47 @@ +"""Integration tests: paranoid read — invalid DB values don't crash the API.""" +from __future__ import annotations + +import asyncio + +import asyncpg +import pytest + +from tests.utils import ADMIN_HEADERS + + +@pytest.mark.asyncio +async def test_paranoid_read_invalid_value_passes_through(service_client, pgsql): + """Manually corrupt a config value in DB; GET should return it as-is (not 500).""" + # Create a valid config + create_resp = await service_client.post( + "/api/v1/config", + json={ + "service_name": "paranoid-svc", + "key": "paranoid_flag", + "config_type": "feature_flag", + "value": {"enabled": True}, + }, + headers=ADMIN_HEADERS, + ) + assert create_resp.status == 201 + config_id = (await create_resp.json())["id"] + + # Directly corrupt the value in the database + conninfo = pgsql["config_service"].conninfo + conn = await asyncpg.connect(conninfo.get_uri()) + try: + await conn.execute( + "UPDATE configs SET value = $1::jsonb WHERE id = $2::uuid", + '{"enabled": "not-a-boolean"}', + config_id, + ) + finally: + await conn.close() + + # GET should succeed (paranoid mode: return as-is, don't raise) + get_resp = await service_client.get( + f"/api/v1/config/{config_id}", headers=ADMIN_HEADERS + ) + assert get_resp.status == 200 + data = await get_resp.json() + assert data["value"] == {"enabled": "not-a-boolean"} diff --git a/projects/backend/services/config-service/tests/test_schema_compat.py b/projects/backend/services/config-service/tests/test_schema_compat.py new file mode 100644 index 00000000..346a24fa --- /dev/null +++ b/projects/backend/services/config-service/tests/test_schema_compat.py @@ -0,0 +1,103 @@ +"""Unit tests for schema compat checker — one test per additive/breaking rule.""" +from __future__ import annotations + +import pytest + +from config_service.services.schema_service import _check_compat + + +BASE_SCHEMA = { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": {"type": "boolean"}, + "count": {"type": "integer", "minimum": 0, "maximum": 100}, + "mode": {"enum": ["read", "write"]}, + }, +} + + +# ─── Additive (allowed) changes ────────────────────────────────────────────── + + +def test_additive_new_optional_field(): + new = {**BASE_SCHEMA, "properties": {**BASE_SCHEMA["properties"], "label": {"type": "string"}}} + assert _check_compat(BASE_SCHEMA, new) == [] + + +def test_additive_remove_required(): + new = {**BASE_SCHEMA, "required": []} + assert _check_compat(BASE_SCHEMA, new) == [] + + +def test_additive_raise_maximum(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["count"] = {"type": "integer", "minimum": 0, "maximum": 200} + new = {**BASE_SCHEMA, "properties": new_props} + assert _check_compat(BASE_SCHEMA, new) == [] + + +def test_additive_lower_minimum(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["count"] = {"type": "integer", "minimum": -5, "maximum": 100} + new = {**BASE_SCHEMA, "properties": new_props} + assert _check_compat(BASE_SCHEMA, new) == [] + + +def test_additive_expand_enum(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["mode"] = {"enum": ["read", "write", "admin"]} + new = {**BASE_SCHEMA, "properties": new_props} + assert _check_compat(BASE_SCHEMA, new) == [] + + +def test_no_changes(): + assert _check_compat(BASE_SCHEMA, BASE_SCHEMA) == [] + + +# ─── Breaking (forbidden) changes ──────────────────────────────────────────── + + +def test_breaking_add_required_field(): + new = {**BASE_SCHEMA, "required": ["enabled", "count"]} + violations = _check_compat(BASE_SCHEMA, new) + assert any("required" in v.lower() for v in violations) + + +def test_breaking_lower_maximum(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["count"] = {"type": "integer", "minimum": 0, "maximum": 50} + new = {**BASE_SCHEMA, "properties": new_props} + violations = _check_compat(BASE_SCHEMA, new) + assert any("maximum" in v for v in violations) + + +def test_breaking_raise_minimum(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["count"] = {"type": "integer", "minimum": 10, "maximum": 100} + new = {**BASE_SCHEMA, "properties": new_props} + violations = _check_compat(BASE_SCHEMA, new) + assert any("minimum" in v for v in violations) + + +def test_breaking_remove_enum_value(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["mode"] = {"enum": ["read"]} + new = {**BASE_SCHEMA, "properties": new_props} + violations = _check_compat(BASE_SCHEMA, new) + assert any("enum" in v for v in violations) + + +def test_breaking_remove_field(): + new_props = {k: v for k, v in BASE_SCHEMA["properties"].items() if k != "count"} + new = {**BASE_SCHEMA, "properties": new_props} + violations = _check_compat(BASE_SCHEMA, new) + assert any("removed" in v.lower() for v in violations) + + +def test_breaking_change_type(): + new_props = {**BASE_SCHEMA["properties"]} + new_props["enabled"] = {"type": "string"} + new = {**BASE_SCHEMA, "properties": new_props} + violations = _check_compat(BASE_SCHEMA, new) + assert any("type" in v for v in violations) diff --git a/projects/backend/services/config-service/tests/test_validation.py b/projects/backend/services/config-service/tests/test_validation.py new file mode 100644 index 00000000..dad68393 --- /dev/null +++ b/projects/backend/services/config-service/tests/test_validation.py @@ -0,0 +1,155 @@ +"""Unit tests for ValidationService (no DB required — mocked schema_repo).""" +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from config_service.core.exceptions import ConfigValidationError +from config_service.domain.enums import ConfigType +from config_service.domain.models import ConfigSchema +from config_service.services.validation_service import ValidationService +import uuid +from datetime import datetime, timezone + + +def _make_schema_repo(schema_dict: dict) -> MagicMock: + repo = MagicMock() + schema_obj = ConfigSchema( + id=uuid.uuid4(), + config_type=ConfigType.feature_flag, + schema=schema_dict, + version=1, + is_active=True, + created_by="system", + created_at=datetime.now(tz=timezone.utc), + ) + repo.get_active = AsyncMock(return_value=schema_obj) + return repo + + +FEATURE_FLAG_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["enabled"], + "properties": {"enabled": {"type": "boolean"}}, + "additionalProperties": False, +} + +QOS_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["__default__"], + "additionalProperties": {"$ref": "#/$defs/qosSettings"}, + "properties": {"__default__": {"$ref": "#/$defs/qosSettings"}}, + "$defs": { + "qosSettings": { + "type": "object", + "required": ["timeout_ms", "retries"], + "properties": { + "timeout_ms": {"type": "integer", "minimum": 1, "maximum": 600000}, + "retries": {"type": "integer", "minimum": 0, "maximum": 10}, + }, + "additionalProperties": False, + } + }, +} + + +@pytest.mark.asyncio +async def test_feature_flag_valid(): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + # Should not raise + await svc.validate_strict(ConfigType.feature_flag, {"enabled": True}) + + +@pytest.mark.asyncio +async def test_feature_flag_missing_enabled(): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + with pytest.raises(ConfigValidationError): + await svc.validate_strict(ConfigType.feature_flag, {}) + + +@pytest.mark.asyncio +async def test_feature_flag_wrong_type(): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + with pytest.raises(ConfigValidationError): + await svc.validate_strict(ConfigType.feature_flag, {"enabled": "yes"}) + + +@pytest.mark.asyncio +async def test_feature_flag_additional_properties_not_allowed(): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + with pytest.raises(ConfigValidationError): + await svc.validate_strict(ConfigType.feature_flag, {"enabled": True, "extra": 1}) + + +@pytest.mark.asyncio +async def test_qos_valid(): + repo = _make_schema_repo(QOS_SCHEMA) + repo.get_active = AsyncMock( + return_value=ConfigSchema( + id=uuid.uuid4(), + config_type=ConfigType.qos, + schema=QOS_SCHEMA, + version=1, + is_active=True, + created_by="system", + created_at=datetime.now(tz=timezone.utc), + ) + ) + svc = ValidationService(repo) + await svc.validate_strict( + ConfigType.qos, + { + "__default__": {"timeout_ms": 150, "retries": 2}, + "/v1/verify": {"timeout_ms": 10000, "retries": 5}, + }, + ) + + +@pytest.mark.asyncio +async def test_qos_missing_default(): + repo = _make_schema_repo(QOS_SCHEMA) + repo.get_active = AsyncMock( + return_value=ConfigSchema( + id=uuid.uuid4(), + config_type=ConfigType.qos, + schema=QOS_SCHEMA, + version=1, + is_active=True, + created_by="system", + created_at=datetime.now(tz=timezone.utc), + ) + ) + svc = ValidationService(repo) + with pytest.raises(ConfigValidationError): + await svc.validate_strict(ConfigType.qos, {"/v1/verify": {"timeout_ms": 100, "retries": 1}}) + + +@pytest.mark.asyncio +async def test_paranoid_logs_metric_but_does_not_raise(monkeypatch): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + # Should not raise even with invalid value + await svc.validate_paranoid(ConfigType.feature_flag, {"enabled": "not-a-bool"}, "cfg-123") + + +@pytest.mark.asyncio +async def test_cache_invalidation(): + repo = _make_schema_repo(FEATURE_FLAG_SCHEMA) + svc = ValidationService(repo) + # First call populates cache + await svc.validate_strict(ConfigType.feature_flag, {"enabled": True}) + assert repo.get_active.call_count == 1 + # Second call uses cache + await svc.validate_strict(ConfigType.feature_flag, {"enabled": False}) + assert repo.get_active.call_count == 1 + # After invalidation, re-fetches + svc.invalidate_cache(ConfigType.feature_flag) + await svc.validate_strict(ConfigType.feature_flag, {"enabled": True}) + assert repo.get_active.call_count == 2 diff --git a/projects/backend/services/config-service/tests/utils.py b/projects/backend/services/config-service/tests/utils.py new file mode 100644 index 00000000..d2a07a9c --- /dev/null +++ b/projects/backend/services/config-service/tests/utils.py @@ -0,0 +1,45 @@ +"""Test helpers.""" +from __future__ import annotations + + +def make_headers( + user_id: str = "test-user", + is_superadmin: bool = False, + system_permissions: list[str] | None = None, + project_permissions: dict[str, list[str]] | None = None, +) -> dict[str, str]: + headers = {"X-User-Id": user_id} + if is_superadmin: + headers["X-User-Is-Superadmin"] = "true" + if system_permissions: + headers["X-User-System-Permissions"] = ",".join(system_permissions) + if project_permissions: + parts = [ + f"{proj}:{','.join(perms)}" + for proj, perms in project_permissions.items() + ] + headers["X-User-Permissions"] = ";".join(parts) + return headers + + +ADMIN_HEADERS = make_headers( + user_id="admin-user", + is_superadmin=True, +) + +VIEWER_HEADERS = make_headers( + user_id="viewer-user", + system_permissions=["configs.view"], +) + +EDITOR_HEADERS = make_headers( + user_id="editor-user", + system_permissions=[ + "configs.view", + "configs.create", + "configs.update", + "configs.delete", + "configs.activate", + "configs.rollback", + ], +)