diff --git a/.env.test b/.env.test index fb58e84..df5f0ab 100644 --- a/.env.test +++ b/.env.test @@ -12,6 +12,11 @@ TASKS_DB_HOST = db_tasks_app_test TASKS_DB_PORT = 5432 TASKS_DB_NAME = tasks_app_test +# Kafka +KAFKA_BOOTSTRAP = kafka_test:9092 +KAFKA_TOPIC = task_events_test +KAFKA_CLIENT_ID = tasks_app_test + # Loki LOKI_PORT=3100 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f63a75..da6a316 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: ["main"] + branches: [main, experiment/workflows] pull_request: - branches: ["main"] + branches: [main] jobs: tests: @@ -17,8 +17,8 @@ jobs: - name: Build and start test services run: docker compose -f docker-compose.test.yml --env-file ./tasks/.env.test up -d --build - - name: Run pytest inside tasks_app_test - run: docker compose -f docker-compose.test.yml --env-file ./tasks/.env.test exec -T tasks_app_test pytest -v + - name: Run tests + run: docker compose -f docker-compose.test.yml --env-file ./tasks/.env.test exec -T tasks_app_test pytest - name: Stop and remove test services if: always() diff --git a/Makefile b/Makefile index ba3e42d..b973829 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,6 @@ build: docker compose up --build -d test: - docker compose -f docker-compose.test.yml up --build -d --remove-orphans + docker compose -f docker-compose.test.yml up --build -d docker compose -f docker-compose.test.yml exec -it tasks_app_test bash -c "pytest -v" - docker compose -f docker-compose.test.yml down + docker compose -f docker-compose.test.yml down -v --remove-orphans diff --git a/README.md b/README.md index 8126bd8..20a0343 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ - **Loki** - система логирования - **Grafana** - система мониторинга - **Promtail** - инструмент для сбора логов +- **Kafka** - система обмена сообщениями +- **Kafka-UI** - визуальный интерфейс для работы с Kafka +- **Locust** - инструмент для нагрузочного тестирования ## 🚀 Запуск проекта @@ -35,9 +38,7 @@ cd microservices-example ``` -2. Создайте файл `.env` на основе `.env.test`: - -3. Запустите приложение с помощью Makefile: +2. Запустите приложение с помощью Makefile: ```bash make build @@ -51,14 +52,19 @@ make test ## 📚 Документация API -После запуска документация API будет доступна по адресу: +После запуска документация API Gateway будет доступна по адресу: - Swagger UI: http://localhost:5000/docs/ ![gateway/static/swagger-custom.png](gateway/static/swagger-custom.png) -## 🔧 Настройка окружения +Также API Gateway предоставляет возможность работы с API через GraphQL: + +- GraphQL UI: http://localhost:5000/api/v1/graphql + ![gateway/static/graphql-interface.png](gateway/static/graphql-interface.png) + +## 🔧 Настройка окружения для разработки -Создайте файл `.env` в корне проекта и в каждом сервисе +Создайте файл `.env` в корне сервиса `gateway/`, `tasks/`. Аналогично файлу `.env.test`. @@ -66,14 +72,34 @@ make test MODE = DEVELOPMENT ``` -## 📊 Логирование +## 📊 Логирование в Grafana Логи приложения отправляются в Loki и доступны через Grafana. **Grafana**: `http://localhost:3010` +`Логин` - admin +`Пароль` - admin (при первом входе) + ### Подключение Loki к Grafana -**Loki-connection-url**: `http://loki:3100` +#### Путь для подключения Loki к Grafana + +`Connections` => `Loki` => `Add new data source` + +- **Connection-url**: `http://loki:3100` +- ⚙️ **Save & Test** + +#### Путь для просмотра логов в Grafana + +`Drilldown` => `Logs` + +## 🎯 Подключение к Kafka-UI + +**Kafka-UI**: `http://localhost:8080` + +## 🧪 Запуск нагрузочного тестирования Locust + +**Locust**: `http://localhost:8089` ## 📄 Лицензия diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 8ab8eda..0492399 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -18,6 +18,27 @@ services: depends_on: db_tasks_app_test: condition: service_healthy + kafka_test: + condition: service_healthy + + tasks_app_worker_test: + build: + context: tasks/ + container_name: tasks_app_worker_test + env_file: + - ./tasks/.env.test + environment: + SERVICE_NAME: tasks_app_test + KAFKA_BOOTSTRAP: kafka_test:9092 + entrypoint: ["sh", "-c"] + command: + - | + python worker.py + depends_on: + db_tasks_app_test: + condition: service_healthy + kafka_test: + condition: service_healthy db_tasks_app_test: image: postgres:17 @@ -38,5 +59,30 @@ services: timeout: 5s retries: 5 + kafka_test: + image: bitnami/kafka:latest + container_name: kafka_test + environment: + KAFKA_BROKER_ID: 1 + KAFKA_CFG_NODE_ID: 1 + KAFKA_CFG_PROCESS_ROLES: "controller,broker" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka_test:9093" + KAFKA_CFG_LISTENERS: "CONTROLLER://:9093,PLAINTEXT://:9092" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka_test:9092" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + KAFKA_CFG_LOG_RETENTION_HOURS: "72" # 3 дня + ports: + - "9092:9092" + - "9093:9093" + healthcheck: + test: ["CMD", "bash", "-c", "kafka-topics.sh --list --bootstrap-server localhost:9092"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 20s + volumes: db_tasks_app_test: diff --git a/docker-compose.yml b/docker-compose.yml index 5456356..bcc8aeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,11 @@ services: context: gateway/ container_name: gateway_app env_file: - - ./gateway/.env + - ./gateway/.env.docker ports: - "${GATEWAY_PORT}:${GATEWAY_PORT}" + networks: + - appnet entrypoint: ["sh", "-c"] command: - | @@ -16,13 +18,34 @@ services: depends_on: loki: condition: service_started + kafka: + condition: service_healthy + + locust_gateway: + build: + context: gateway/ + container_name: locust_gateway + env_file: + - ./gateway/.env.docker + ports: + - "8089:8089" + networks: + - appnet + entrypoint: ["sh", "-c"] + command: + - | + poetry run locust -f locustfile.py \ + --host http://gateway_app:${GATEWAY_PORT} + depends_on: + gateway_app: + condition: service_started tasks_app: build: context: tasks/ container_name: tasks_app env_file: - - ./tasks/.env + - ./tasks/.env.docker ports: - "${TASKS_APP_PORT}" entrypoint: ["sh", "-c"] @@ -38,12 +61,41 @@ services: condition: service_healthy loki: condition: service_started + kafka: + condition: service_healthy + networks: + - appnet + + tasks_app_worker: + build: + context: tasks/ + container_name: tasks_app_worker + env_file: + - ./tasks/.env.docker + environment: + SERVICE_NAME: tasks_app + KAFKA_BOOTSTRAP: kafka:9092 + KAFKA_GROUP_ID: tasks_app_group + entrypoint: ["sh", "-c"] + command: + - | + python worker.py + depends_on: + db_tasks_app: + condition: service_healthy + kafka: + condition: service_healthy + networks: + - appnet + deploy: + restart_policy: + condition: on-failure db_tasks_app: image: postgres:17 container_name: db_tasks_app env_file: - - ./tasks/.env + - ./tasks/.env.docker environment: POSTGRES_USER: ${TASKS_DB_USER} POSTGRES_PASSWORD: ${TASKS_DB_PASS} @@ -52,16 +104,64 @@ services: - "${TASKS_DB_PORT}" volumes: - db_tasks_app:/var/lib/postgresql/data + networks: + - appnet healthcheck: test: ["CMD-SHELL", "pg_isready -U ${TASKS_DB_USER}"] interval: 5s timeout: 5s retries: 5 - + + kafka: + image: bitnami/kafka:latest + container_name: kafka + # volumes: + # - kafka_data:/bitnami/kafka + environment: + KAFKA_BROKER_ID: 1 + KAFKA_CFG_NODE_ID: 1 + KAFKA_CFG_PROCESS_ROLES: "controller,broker" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" + KAFKA_CFG_LISTENERS: "CONTROLLER://:9093,PLAINTEXT://:9092" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + KAFKA_CFG_LOG_RETENTION_HOURS: "72" # 3 дня + ports: + - "9092:9092" + - "9093:9093" + healthcheck: + test: ["CMD", "bash", "-c", "kafka-topics.sh --list --bootstrap-server localhost:9092"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 20s + networks: + - appnet + + kafka-ui: + image: provectuslabs/kafka-ui:latest + container_name: kafka-ui + ports: + - "8088:8080" + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + KAFKA_CLUSTERS_0_READONLY: "false" + depends_on: + kafka: + condition: service_healthy + networks: + - appnet + loki: image: grafana/loki:latest ports: - "${LOKI_PORT}" + networks: + - appnet volumes: - ./loki-config.yaml:/etc/loki/local-config.yaml - loki_data:/loki @@ -73,6 +173,8 @@ services: promtail: image: grafana/promtail:latest + networks: + - appnet volumes: - ./promtail-config.yaml:/etc/promtail/config.yaml - /var/run/docker.sock:/var/run/docker.sock @@ -87,6 +189,8 @@ services: grafana: image: grafana/grafana:latest + networks: + - appnet ports: - "${GRAFANA_PORT}:3000" volumes: @@ -96,7 +200,12 @@ services: - GF_SECURITY_ADMIN_PASSWORD=admin - GF_USERS_ALLOW_SIGN_UP=false +networks: + appnet: + driver: bridge + volumes: db_tasks_app: loki_data: grafana_data: + # kafka_data: diff --git a/docs/architecture-ru.md b/docs/architecture-ru.md new file mode 100644 index 0000000..2c429dc --- /dev/null +++ b/docs/architecture-ru.md @@ -0,0 +1,105 @@ +# Архитектура приложения + +## Обзор + +Приложение построено по микросервисной архитектуре с четким разделением ответственности между компонентами. Основные сервисы взаимодействуют через API и брокер сообщений (Kafka). + +## Диаграмма компонентов + +```mermaid +graph TD + A[Клиент] -->|HTTP/HTTPS| B[API Gateway] + B -->|HTTP| C[Сервис задач Tasks] + C -->|SQL| D[(База данных)] + B -->|Kafka| F[Брокер сообщений] + F -->|События| G[Потребители событий] + F -->|События| E[Другие сервисы] + F -->|События| C[Сервис задач] + B -->|HTTP| E[Другие сервисы] +``` + +## Основные компоненты + +### 1. API Gateway + +- Единая точка входа для всех клиентских запросов +- Маршрутизация запросов к соответствующим сервисам + +#### В дальнейшем планируется добавить: + +- Аутентификация и авторизация +- Балансировка нагрузки +- Кеширование + +### 2. Сервис задач (Tasks) + +#### Основные функции: + +- Управление задачами (CRUD операции) +- Валидация данных +- Бизнес-логика работы с задачами +- Интеграция с другими сервисами через события + +### 3. База данных + +- Хранение данных приложения + +### 4. Брокер сообщений (Kafka) + +- Асинхронная коммуникация между сервисами +- Обработка событий +- Обеспечение надежности доставки сообщений + +### 1. Создание задачи + +```mermaid +sequenceDiagram + participant C as Клиент + participant G as Gateway + participant T as Tasks Service + participant D as База данных + + C->>G: POST /api/v1/task/create + G->>T: Перенаправление запроса + T->>D: Валидация и сохранение + D-->>T: Успешное сохранение + T-->>G: 201 Created + G-->>C: Ответ +``` + +### 2. Обновление статуса задачи + +```mermaid +sequenceDiagram + participant C as Клиент + participant G as Gateway + participant T as Tasks Service + participant D as База данных + + C->>G: PATCH /api/v1/tasks/{id} + G->>T: Перенаправление запроса + T->>D: Частичное обновление задачи + D-->>T: Данные задачи + T-->>G: 200 OK + T-->>C: Обновленная задача +``` + +### 3. Создание задачи через Kafka + +```mermaid +sequenceDiagram + participant C as Клиент + participant G as Gateway + participant K as Kafka + participant R as Tasks Repository + participant D as База данных + + C->>G: POST /api/v1/task/create_event + G->>K: Отправка события TaskCreation + K-->>C: 201 Created, json.response {'status': 'published'} + K->>R: Создание задачи + R->>D: Валидация и сохранение + D-->>R: Успешное сохранение + R-->>K: commit(TaskCreation) + +``` diff --git a/docs/data-access-patterns-ru.md b/docs/data-access-patterns-ru.md new file mode 100644 index 0000000..6f1ba6b --- /dev/null +++ b/docs/data-access-patterns-ru.md @@ -0,0 +1,78 @@ +# Паттерны доступа к данным + +## Unit of Work (Единица работы) + +### Реализация в проекте + +В проекте используется асинхронная реализация Unit of Work, которая управляет сессией базы данных и координацией транзакций. + +**Ключевые файлы:** + +- `tasks/infrastructure/database/uow.py` - реализация UoW +- `tasks/infrastructure/database/db_connect.py` - настройка подключения к БД + +### Основные компоненты + +1. **Класс UnitOfWork** + + ```python + class UnitOfWork: + def __init__(self, session: AsyncSession): + self.session: AsyncSession = session + # доступ ко всем репозиториям + self.tasks: TaskRepository = TaskRepository(self.session) + + async def commit(self) -> None: + await self.session.commit() + + async def rollback(self) -> None: + await self.session.rollback() + + async def close(self) -> None: + await self.session.close() + ``` + +2. **Контекстный менеджер** + ```python + @asynccontextmanager + async def unit_of_work() -> AsyncIterator[UnitOfWork]: + """Контекстный менеджер для работы с Unit of Work""" + session = async_session() + uow = UnitOfWork(session) + try: + yield uow + await uow.commit() + except IntegrityError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка целостности данных', exc_info=e) + raise ValueError('UOW: Ошибка: нарушение целостности данных') from e + except OperationalError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка подключения к БД', exc_info=e) + raise ConnectionError('UOW: Ошибка подключения к базе данных') from e + except TimeoutError as e: + await uow.rollback() + logger.exception('Unit of work: Таймаут операции с БД', exc_info=e) + raise TimeoutError('UOW: Превышено время ожидания ответа от БД') from e + except SQLAlchemyError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка выполнения запроса', exc_info=e) + raise RuntimeError('UOW: Ошибка при выполнении запроса к БД') from e + except Exception as e: + if isinstance(e, HTTPException) and getattr(e, 'status_code', 404) == 404: + # Пропускаем исключения и передаём вверх по стеку вызовов + logger.debug('Unit of work: пропускаем бизнес/клиентскую ошибку', exc_info=e) + raise + await uow.rollback() + logger.critical('Unit of work: Непредвиденная ошибка', exc_info=e) + raise + finally: + await uow.close() + ``` + +### Преимущества реализации + +- **Асинхронная работа** с базой данных +- **Автоматическое управление** транзакциями +- **Централизованная обработка** ошибок +- **Удобное использование** через контекстный менеджер diff --git a/docs/kafka-configuration-ru.md b/docs/kafka-configuration-ru.md new file mode 100644 index 0000000..59d3b61 --- /dev/null +++ b/docs/kafka-configuration-ru.md @@ -0,0 +1,329 @@ +# Руководство по настройке Apache Kafka (bitnami/kafka) + +Этот документ содержит полное руководство по настройке Apache Kafka с акцентом на режиме KRaft, используемом в данном проекте. + +## Содержание + +1. [Обзор режима KRaft](#обзор-режима-kraft) +2. [Основные настройки](#основные-настройки) +3. [Настройки хранения сообщений](#настройки-хранения-сообщений) +4. [Оптимизация производительности](#оптимизация-производительности) +5. [Мониторинг и обслуживание](#мониторинг-и-обслуживание) + +## Сравнение пакетов контейнеров (bitnami/kafka и apache/kafka) + +Основные отличия между `bitnami/kafka` и `apache/kafka`: + +1. **Базовый образ**: + + - `bitnami/kafka` использует минимальный образ на основе Debian + - `apache/kafka` использует образ на основе Ubuntu + +2. **Управление процессом**: + + - Bitnami использует `tini` для корректной обработки сигналов + - Apache Kafka использует скрипты обертки + +3. **Безопасность**: + + - Bitnami запускает Kafka от непривилегированного пользователя `1001` по умолчанию + - Apache Kafka может требовать настройки прав вручную + +4. **Переменные окружения**: + + - Bitnami использует префикс `KAFKA_` для всех настроек + - Apache Kafka может требовать ручного конфигурирования файлов + +5. **Версионирование**: + + - Bitnami выпускает обновления быстрее + - Apache Kafka обновляется реже, но официально + +6. **Дополнительные утилиты**: + + - Bitnami включает дополнительные скрипты для управления + - Apache Kafka предоставляет только базовый функционал + +7. **Размер образа**: + - `bitnami/kafka` обычно легче + - `apache/kafka` может быть больше из-за включенных зависимостей + +Для продакшена Bitnami часто предпочтительнее из-за лучшей безопасности и удобства настройки. + +## Обзор режима KRaft + +### Что такое KRaft? + +KRaft (Kafka Raft Metadata Mode) — это новый протокол консенсуса, который устраняет необходимость в ZooKeeper для Apache Kafka. Он был представлен для упрощения архитектуры Kafka и улучшения её масштабируемости. + +### Ключевые отличия от Kafka с ZooKeeper + +| Характеристика | Режим KRaft | Режим с ZooKeeper | +| ---------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- | +| **Архитектура** | Один тип процесса (брокер + контроллер) | Требуется отдельный ансамбль ZooKeeper | +| **Масштабируемость** | Лучшая обработка метаданных для больших кластеров | Может стать узким местом при большом количестве топиков/партиций | +| **Развертывание** | Более простое развертывание с меньшим количеством компонентов | Требует отдельного управления ZooKeeper | +| **Производительность** | Меньшая задержка операций с метаданными | Большая задержка из-за координации через ZooKeeper | +| **Зрелость** | Готово к продакшену начиная с Kafka 3.3 | Проверено временем, но сложнее в настройке | + +## Основные настройки + +### Идентификация брокера + +```yaml +KAFKA_BROKER_ID: 1 # Уникальный идентификатор брокера в кластере +KAFKA_CFG_NODE_ID: 1 # Идентификатор ноды в режиме KRaft +``` + +### Сеть и безопасность + +```yaml +KAFKA_CFG_LISTENERS: "CONTROLLER://:9093,PLAINTEXT://:9092" +KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" +KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092" +KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER" +KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" +``` + +### Настройки KRaft + +```yaml +KAFKA_CFG_PROCESS_ROLES: "controller,broker" # Роли ноды в режиме KRaft +KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" # Формат: {id}@{хост}:{порт} +``` + +## Настройки хранения сообщений + +### Временное хранение + +```yaml +# Хранить сообщения 7 дней (по умолчанию) +KAFKA_CFG_LOG_RETENTION_HOURS: "168" +# Альтернатива в миллисекундах +# KAFKA_CFG_LOG_RETENTION_MS: "604800000" +``` + +### Ограничение по размеру + +```yaml +# Без ограничения по размеру +KAFKA_CFG_LOG_RETENTION_BYTES: "-1" +# Или установите лимит (например, 10 ГБ) +# KAFKA_CFG_LOG_RETENTION_BYTES: "10737418240" +``` + +### Управление сегментами логов + +```yaml +# Максимальный размер одного файла сегмента (1 ГБ) +KAFKA_CFG_LOG_SEGMENT_BYTES: "1073741824" + +# Максимальное время до создания нового сегмента (7 дней) +KAFKA_CFG_LOG_SEGMENT_MS: "604800000" + +# Как часто проверять сегменты для удаления (5 минут) +KAFKA_CFG_LOG_RETENTION_CHECK_INTERVAL_MS: "300000" +``` + +## Оптимизация производительности + +### Настройки памяти + +```yaml +# Размер кучи (настройте в зависимости от доступной памяти) +KAFKA_HEAP_OPTS: "-Xmx4G -Xms4G" + +# Прямая память для сетевых буферов +KAFKA_OPTS: "-XX:MaxDirectMemorySize=1G" +``` + +### Ввод-вывод и пропускная способность + +```yaml +# Количество потоков для обработки сетевых запросов +# Рекомендуемое значение: (количество ядер CPU * 2) + 1 +KAFKA_CFG_NUM_NETWORK_THREADS: "3" + +# Количество потоков для операций ввода-вывода +# Рекомендуемое значение: количество ядер * 2 +KAFKA_CFG_NUM_IO_THREADS: "8" + +# Количество потоков для репликации данных между брокерами +# Рекомендуемое значение: количество ядер / 2 +KAFKA_CFG_NUM_REPLICA_FETCHERS: "1" + +# Размер буфера сокета (в байтах) +KAFKA_CFG_SOCKET_RECEIVE_BUFFER_BYTES: "102400" + +# Максимальный размер запроса (в байтах) +KAFKA_CFG_MESSAGE_MAX_BYTES: "1048588" # 1 МБ + +# Максимальный размер пакета (в байтах) +KAFKA_CFG_SOCKET_REQUEST_MAX_BYTES: "104857600" # 100 МБ +``` + +### Оптимизация для высоконагруженных систем + +```yml +# Увеличиваем буферы для высоконагруженных систем +KAFKA_CFG_SOCKET_RECEIVE_BUFFER_BYTES: "1048576" # 1 МБ +KAFKA_CFG_SOCKET_SEND_BUFFER_BYTES: "1048576" # 1 МБ + +# Настройки для большого количества подключений +KAFKA_CFG_MAX_CONNECTIONS_PER_IP: "2147483647" +KAFKA_CFG_MAX_CONNECTIONS_PER_IP_OVERRIDES: "127.0.0.1:PLAINTEXT:100" +``` + +## Топик + +```yml +# Автоматическое создание топиков +KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" +``` + +### Что делает: + +- Если true (по умолчанию): Kafka автоматически создает топик при первой публикации сообщения в несуществующий топик. +- Если false: Kafka вернет ошибку при попытке отправить сообщение в несуществующий топик. + +### Плюсы включения (true): + +- Удобство разработки - не нужно создавать топики вручную +- Гибкость - приложения могут создавать топики "на лету" + +### Минусы включения: + +- Безопасность - любое приложение может создать топик +- Опечатки - можно случайно создать дубликат топика из-за опечатки в названии +- Неконтролируемый рост - может привести к созданию множества ненужных топиков + +### Рекомендации: + +- Для разработки: можно оставить true для удобства +- Для продакшена: лучше установить false и создавать топики вручную через: + +```bash +docker exec -it kafka kafka-topics.sh --create \ + --bootstrap-server localhost:9092 \ + --replication-factor 1 \ + --partitions 1 \ + --topic <ваш_топик> +``` + +## Мониторинг и обслуживание + +### Проверка работоспособности + +```yml +healthcheck: + test: + [ + "CMD", + "bash", + "-c", + "kafka-topics.sh --list --bootstrap-server localhost:9092", + ] + interval: 10s + timeout: 5s + retries: 6 + start_period: 20s +``` + +### Полезные команды + +#### Проверка настроек брокеров + +```bash +docker exec -it kafka bash -c "kafka-configs.sh --describe --all --bootstrap-server localhost:9092 --entity-type brokers" +``` + +#### Проверка конфигурации топика + +```bash +docker exec -it kafka kafka-configs.sh --describe --all --bootstrap-server localhost:9092 +``` + +#### Мониторинг групп потребителей + +```bash +docker exec -it kafka kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list +``` + +#### Проверка хранения сообщений + +```bash +docker exec -it kafka kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe --topic-list ваш-топик +``` + +#### Проверка загрузки потоков + +```bash +docker exec -it kafka kafka-run-class.sh kafka.tools.JmxTool --object-name kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent --jmx-url service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi +``` + +### Проверка очереди запросов + +```bash +docker exec -it kafka kafka-run-class.sh kafka.tools.JmxTool --object-name kafka.network:type=RequestChannel,name=RequestQueueSize --jmx-url service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi +``` + +### Текущие настройки ретеншена + +```bash +docker exec -it kafka kafka-configs.sh --describe --bootstrap-server localhost:9092 --entity-type brokers --entity-default +``` + +## Скрипты + +### Удаление записей + +#### Инструмент для принудительного удаления записей из топиков Kafka + +```bash +# Создаем JSON-файл с указанием топика и оффсета, до которого нужно удалить записи +cat > delete-records.json << 'EOF' +{ + "partitions": [ + { + "topic": "your_topic_name", + "partition": 0, + "offset": 100 # Удалить все сообщения до этого оффсета (не включительно) + } + ], + "version": 1 +} +EOF +``` + +#### Запускаем удаление записей + +```bash +docker exec -it kafka kafka-delete-records.sh \ + --bootstrap-server localhost:9092 \ + --offset-json-file delete-records.json +``` + +## Рекомендации + +1. **Для продакшена**: + + - Используйте минимум 3 ноды для отказоустойчивости + - Установите подходящую политику хранения в зависимости от доступного места + - Мониторьте использование диска и отставание потребителей + +2. **Для разработки**: + + - Установите меньшее время хранения для экономии места + - Включите автоматическое создание топиков для удобства + - Используйте незащищенный протокол (PLAINTEXT) только в разработке + +3. **Для KRaft**: + - Убедитесь в правильности настройки `node.id` и `process.roles` + - Следите за состоянием контроллера в режиме KRaft + - Контролируйте рост лога метаданных + +## Ссылки + +- [Документация Apache Kafka](https://kafka.apache.org/documentation/) +- [Документация по KRaft](https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum) +- [Настройка Kafka](https://kafka.apache.org/documentation/#configuration) diff --git a/docs/monitoring-ru.md b/docs/monitoring-ru.md new file mode 100644 index 0000000..c9adf85 --- /dev/null +++ b/docs/monitoring-ru.md @@ -0,0 +1,65 @@ +# Мониторинг и логирование + +## Обзор + +В проекте реализована система логирования на основе стандартного модуля `logging` с JSON-форматированием. Логи выводятся в консоль в структурированном виде, что позволяет легко их анализировать с помощью различных инструментов. + +## Конфигурация логирования + +### Основные компоненты + +1. **Кастомный JSON-форматтер** (`CustomJsonFormatter`) + + - Форматирует логи в JSON + - Добавляет метаданные (время, уровень, сервис и т.д.) + - Поддерживает исключения и кастомные теги + +2. **Фильтры сервисов** (`ServiceNameFilter`) + - Добавляет имя сервиса в логи + - Позволяет различать логи от разных компонентов системы + +## Использование в коде + +### Инициализация логгера + +```py +# Получение логгера для конкретного модуля +import logging.config +from core.logger import logger_config + +logging.config.dictConfig(logger_config) +kafka_logger = logging.getLogger('kafka_logger') + +# Логирование с дополнительными полями +kafka_logger.info('Сообщение успешно отправлено', extra={ + 'tags': { + 'topic': settings.kafka.TOPIC, + 'event': event['event'], + 'task_title': event['task']['title'], + 'task_description': event['task']['description'], + } +}) +``` + +### Обработка ошибок + +```py +try: + # Код, который может вызвать исключение + await repository.get_task(task_id) +except Exception as e: + logger.error('Failed to get task', exc_info=e) + raise +``` + +## Настройка логгеров + +В проекте настроены следующие логгеры: + +1. **task_repository_logger** - для логирования операций с репозиторием задач +2. **database_logger** - для логирования операций с базой данных +3. **handle_errors_logger** - для логирования ошибок +4. **uvicorn** - для логирования веб-сервера +5. **kafka** - для логирования работы с Kafka + +Каждый логгер имеет свой фильтр, добавляющий соответствующее имя сервиса. diff --git a/gateway/.env.docker b/gateway/.env.docker new file mode 100644 index 0000000..784ef21 --- /dev/null +++ b/gateway/.env.docker @@ -0,0 +1,7 @@ +MODE = PRODUCTION + +# FastAPI +GATEWAY_PORT = 5000 + +# Services +TASKS_MICROSERVICE_URL = http://tasks_app:5001 \ No newline at end of file diff --git a/gateway/.env.test b/gateway/.env.test index b436699..acd33b1 100644 --- a/gateway/.env.test +++ b/gateway/.env.test @@ -1,4 +1,7 @@ MODE = TEST # FastAPI -GATEWAY_PORT = 5000 \ No newline at end of file +GATEWAY_PORT = 5000 + +# Services +TASKS_MICROSERVICE_URL = http://tasks_app_test:5001 \ No newline at end of file diff --git a/gateway/api_v1/graphql/query_loader.py b/gateway/api_v1/graphql/query_loader.py new file mode 100644 index 0000000..b719e69 --- /dev/null +++ b/gateway/api_v1/graphql/query_loader.py @@ -0,0 +1,42 @@ +from pathlib import Path + + +def load_query(query_name: str, section: str) -> str: + """ + Загрузка GraphQL-запроса из файла + + Args: + query_name: Название файла запроса без расширения + section: Тип запроса ('tasks' или 'users') + + Returns: + str: Запрос как строка + """ + base_dir = Path(__file__).parent.parent + query_path = base_dir / 'graphql' / section / 'queries' / f'{query_name}.graphql' + + try: + with open(query_path, 'r') as f: + return f.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f'Запрос не найден: {query_path}') + +def load_mutation(mutation_name: str, section: str) -> str: + """ + Загрузка GraphQL-мутации из файла + + Args: + mutation_name: Название файла мутации без расширения + section: Тип мутации ('tasks' или 'users') + + Returns: + str: Мутация как строка + """ + base_dir = Path(__file__).parent.parent + mutation_path = base_dir / 'graphql' / section / 'mutations' / f'{mutation_name}.graphql' + + try: + with open(mutation_path, 'r') as f: + return f.read().strip() + except FileNotFoundError: + raise FileNotFoundError(f'Мутация не найдена: {mutation_path}') \ No newline at end of file diff --git a/gateway/api_v1/graphql/tasks/mutations/create_task.graphql b/gateway/api_v1/graphql/tasks/mutations/create_task.graphql new file mode 100644 index 0000000..ccfe6ff --- /dev/null +++ b/gateway/api_v1/graphql/tasks/mutations/create_task.graphql @@ -0,0 +1,8 @@ +mutation CreateTask($title: String!, $description: String) { + createTask(input: { title: $title, description: $description }) { + id + title + description + status + } +} diff --git a/gateway/api_v1/graphql/tasks/mutations/delete_task.graphql b/gateway/api_v1/graphql/tasks/mutations/delete_task.graphql new file mode 100644 index 0000000..242f2cc --- /dev/null +++ b/gateway/api_v1/graphql/tasks/mutations/delete_task.graphql @@ -0,0 +1,3 @@ +mutation DeleteTask($id: ID!) { + deleteTask(id: $id) +} diff --git a/gateway/api_v1/graphql/tasks/mutations/update_task.graphql b/gateway/api_v1/graphql/tasks/mutations/update_task.graphql new file mode 100644 index 0000000..450284f --- /dev/null +++ b/gateway/api_v1/graphql/tasks/mutations/update_task.graphql @@ -0,0 +1,8 @@ +mutation UpdateTask($id: ID!, $input: TaskUpdateInputGQL!) { + updateTask(id: $id, input: $input) { + id + title + description + status + } +} diff --git a/gateway/api_v1/graphql/tasks/queries/get_task.graphql b/gateway/api_v1/graphql/tasks/queries/get_task.graphql new file mode 100644 index 0000000..47b4dfb --- /dev/null +++ b/gateway/api_v1/graphql/tasks/queries/get_task.graphql @@ -0,0 +1,8 @@ +query GetTask($id: ID!) { + task(id: $id) { + id + title + description + status + } +} diff --git a/gateway/api_v1/graphql/tasks/queries/get_tasks.graphql b/gateway/api_v1/graphql/tasks/queries/get_tasks.graphql new file mode 100644 index 0000000..4f6371d --- /dev/null +++ b/gateway/api_v1/graphql/tasks/queries/get_tasks.graphql @@ -0,0 +1,12 @@ +query GetTasks($status: TaskStatusGQL, $offset: Int!, $limit: Int!) { + tasks(status: $status, offset: $offset, limit: $limit) { + tasks { + id + title + description + status + } + total + pagesCount + } +} diff --git a/gateway/api_v1/graphql/tasks/resolvers.py b/gateway/api_v1/graphql/tasks/resolvers.py new file mode 100644 index 0000000..2c3ec34 --- /dev/null +++ b/gateway/api_v1/graphql/tasks/resolvers.py @@ -0,0 +1,147 @@ +import strawberry, httpx +from .schemas import ( + TaskGQL, + TaskPageGQL, + TaskCreateInputGQL, + TaskUpdateInputGQL, + TaskStatusGQL, + map_json_to_task_gql +) +from ..query_loader import load_query, load_mutation +from typing import Optional +from core.config import settings + +TASKS_URL = settings.tasks_microservice_url + '/api/v1/graphql' + +@strawberry.type +class Query: + @strawberry.field + async def task( + self, + id: strawberry.ID, + ) -> Optional[TaskGQL]: + query = load_query('get_task', 'tasks') + async with httpx.AsyncClient() as client: + resp = await client.post(TASKS_URL, json={'query': query, 'variables': {'id': id}}) + data = resp.json()['data']['task'] + if data is None: + return None + return map_json_to_task_gql(data) + + @strawberry.field + async def tasks( + self, + status: Optional[TaskStatusGQL] = None, + offset: int = 0, + limit: int = 10, + ) -> TaskPageGQL: + """ + Получение списка задач с поддержкой: + - Фильтрации по статусу + - Пагинации (offset/limit) + """ + query = load_query('get_tasks', 'tasks') + async with httpx.AsyncClient() as client: + variables = { + 'offset': offset, + 'limit': limit + } + if status: + variables['status'] = status.value.upper() + response = await client.post( + TASKS_URL, + json={ + 'query': query, + 'variables': variables + } + ) + data = response.json() + if 'errors' in data: + error_msg = data['errors'][0]['message'] + raise Exception(f'Ошибка получения задач: {error_msg}') + tasks_data = data['data']['tasks'] + return TaskPageGQL( + tasks=[ + map_json_to_task_gql(task) + for task in tasks_data['tasks'] + ], + total=tasks_data['total'], + pages_count=tasks_data['pagesCount'] + ) + +@strawberry.type +class Mutation: + @strawberry.mutation + async def create_task( + self, + input: TaskCreateInputGQL, + ) -> TaskGQL: + """Создание новой задачи""" + query = load_mutation('create_task', 'tasks') + async with httpx.AsyncClient() as client: + response = await client.post( + TASKS_URL, + json={ + 'query': query, + 'variables': { + 'title': input.title, + 'description': input.description + } + } + ) + data = response.json() + if 'errors' in data: + error_msg = data['errors'][0]['message'] + raise Exception(f'Ошибка создания задачи: {error_msg}') + task_data = data['data']['createTask'] + return map_json_to_task_gql(task_data) + + @strawberry.mutation + async def update_task( + self, + id: strawberry.ID, + input: TaskUpdateInputGQL, + ) -> Optional[TaskGQL]: + """Обновление задачи""" + query = load_mutation('update_task', 'tasks') + update_data = input.to_dict() + async with httpx.AsyncClient() as client: + response = await client.post( + TASKS_URL, + json={ + 'query': query, + 'variables': { + 'id': id, + 'input': update_data + } + } + ) + data = response.json() + if 'errors' in data: + error_msg = data['errors'][0]['message'] + raise Exception(f'Ошибка обновления задачи: {error_msg}') + task_data = data['data']['updateTask'] + return map_json_to_task_gql(task_data) if task_data else None + + @strawberry.mutation + async def delete_task( + self, + id: strawberry.ID, + ) -> bool: + """Удаление задачи""" + query = load_mutation('delete_task', 'tasks') + async with httpx.AsyncClient() as client: + response = await client.post( + TASKS_URL, + json={ + 'query': query, + 'variables': { + 'id': id + } + } + ) + data = response.json() + if 'errors' in data: + error_msg = data['errors'][0]['message'] + raise Exception(f'Ошибка удаления задачи: {error_msg}') + return bool(data['data']['deleteTask']) diff --git a/gateway/api_v1/graphql/tasks/schemas.py b/gateway/api_v1/graphql/tasks/schemas.py new file mode 100644 index 0000000..c4489d6 --- /dev/null +++ b/gateway/api_v1/graphql/tasks/schemas.py @@ -0,0 +1,56 @@ +import strawberry +from enum import Enum +from typing import Optional, List + + +@strawberry.enum +class TaskStatusGQL(str, Enum): + CREATED = 'created' + IN_PROGRESS = 'in_progress' + COMPLETED = 'completed' + +@strawberry.type +class TaskGQL: + id: strawberry.ID + title: str + description: Optional[str] = None + status: TaskStatusGQL + +def map_json_to_task_gql(data: dict) -> TaskGQL: + """Карта базы данных на GraphQL""" + return TaskGQL( + id=data['id'], + title=data['title'], + description=data.get('description'), + status=TaskStatusGQL(data['status'].lower()), + ) + +@strawberry.type +class TaskPageGQL: + tasks: List[TaskGQL] + total: int + pages_count: int + +@strawberry.input +class TaskCreateInputGQL: + title: str + description: Optional[str] = None + +@strawberry.input +class TaskUpdateInputGQL: + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TaskStatusGQL] = None + + def to_dict(self) -> dict: + """ + Сериализация в словарь, убирая поля со значениями None. + Значения enum приводятся к верхнему регистру для GraphQL. + """ + fields = { + 'title': self.title, + 'description': self.description, + 'status': self.status.value.upper() if self.status is not None else None, + } + return {k: v for k, v in fields.items() if v is not None} + diff --git a/gateway/api_v1/rest/__init__.py b/gateway/api_v1/rest/__init__.py index 7453f5e..38871f7 100644 --- a/gateway/api_v1/rest/__init__.py +++ b/gateway/api_v1/rest/__init__.py @@ -2,14 +2,11 @@ from .tasks.views import router as tasks_router from .tasks.views import router_list as tasks_list_router - +from .tasks.views import router_worker as tasks_worker_router router = APIRouter() router.include_router(router=tasks_router, prefix='/task') router.include_router(router=tasks_list_router, prefix='/tasks') - - - - +router.include_router(router=tasks_worker_router, prefix='/task') \ No newline at end of file diff --git a/gateway/api_v1/rest/tasks/dependencies.py b/gateway/api_v1/rest/tasks/dependencies.py new file mode 100644 index 0000000..6fa24d7 --- /dev/null +++ b/gateway/api_v1/rest/tasks/dependencies.py @@ -0,0 +1,5 @@ +from aiokafka import AIOKafkaProducer + +async def get_producer() -> AIOKafkaProducer: + from main import app # Ленивый импорт + return app.state.kafka_producer diff --git a/gateway/api_v1/rest/tasks/views.py b/gateway/api_v1/rest/tasks/views.py index 9a8fdb2..8e91f8d 100644 --- a/gateway/api_v1/rest/tasks/views.py +++ b/gateway/api_v1/rest/tasks/views.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter,Query, status, Path +from fastapi import APIRouter,Query, status, Path, Depends from typing import Annotated from .schemas import ( SchemaTask, @@ -9,9 +9,16 @@ TaskFilters ) from infrastructure.tasks_facade import task_facade +from .dependencies import AIOKafkaProducer, get_producer +from core.config import settings +import logging.config +from core.logger import logger_config -router, router_list = APIRouter(tags=['Tasks']), APIRouter(tags=['Tasks']) +logging.config.dictConfig(logger_config) +kafka_logger = logging.getLogger('kafka_logger') + +router, router_list, router_worker = APIRouter(tags=['Task']), APIRouter(tags=['Tasks']), APIRouter(tags=['Task Worker Events']) @router.get('/{task_id}/', response_model=SchemaTask, status_code=status.HTTP_200_OK) @@ -31,7 +38,6 @@ async def get_task(task_id: Annotated[str, Path]): """ return await task_facade.get_task(task_id=task_id) - @router.post('/create', response_model=SchemaTask, status_code=status.HTTP_201_CREATED) async def create_task(task: TaskCreate): """Создает новую задачу. @@ -168,4 +174,28 @@ async def get_list_tasks( limit=filters.limit, column_search=filters.column_search, input_search=filters.input_search, - ) \ No newline at end of file + ) + +@router_worker.post('/create_event', status_code=status.HTTP_201_CREATED) +async def send_task_creation_event( + task: TaskCreate, + producer: AIOKafkaProducer = Depends(get_producer) +): + event = { + 'event': 'TaskCreation', + 'task': task.model_dump() + } + try: + await producer.send_and_wait(topic=settings.kafka.TOPIC, value=event) + kafka_logger.info('Сообщение успешно отправлено', extra={ + 'tags': { + 'topic': settings.kafka.TOPIC, + 'event': event['event'], + 'task_title': event['task']['title'], + 'task_description': event['task']['description'], + } + }) + except Exception as e: + kafka_logger.exception('Ошибка отправки сообщения в topic task_events', exc_info=e) + raise HTTPException(status_code=503, detail=f'Ошибка отправки сообщения в topic task_events: {e}') + return {'status': 'published'} \ No newline at end of file diff --git a/gateway/core/config.py b/gateway/core/config.py index e27352b..439f72e 100644 --- a/gateway/core/config.py +++ b/gateway/core/config.py @@ -1,9 +1,10 @@ # -*- encoding: utf-8 -*- import os +from dotenv import load_dotenv from pydantic import BaseModel from pydantic_settings import BaseSettings - +load_dotenv() class ConfigurationCORS(BaseModel): ######################### # CORS # @@ -25,16 +26,33 @@ class ConfigurationCORS(BaseModel): 'Authorization', 'Access-Control-Allow-Origin', ] + +class ConfigurationKafka(BaseModel): + ######################### + # KAFKA # + ######################### + BOOTSTRAP: str = os.getenv('KAFKA_BOOTSTRAP', 'kafka:9092') + CLIENT_ID: str = os.getenv('KAFKA_CLIENT_ID', 'gateway_app') + TOPIC: str = os.getenv('KAFKA_TOPIC', 'task_events') + GROUP_ID: str = os.getenv('KAFKA_GROUP_ID', 'gateway_app_group') + STARTUP_RETRIES: int = os.getenv('KAFKA_STARTUP_RETRIES', 3) + RETRY_BACKOFF: float = os.getenv('KAFKA_RETRY_BACKOFF', 1.0) class Setting(BaseSettings): + # ENV + MODE: str = os.getenv('MODE', 'DEVELOPMENT') + # FASTAPI api_v1_prefix: str = '/api/v1' api_v1_port: int = os.getenv('GATEWAY_PORT', 5000) # MICROSERVICES - tasks_microservice_url: str = os.getenv('TASKS_MICROSERVICE_URL', 'http://tasks_app:5001') + tasks_microservice_url: str = os.getenv('TASKS_MICROSERVICE_URL', 'http://localhost:5001') # CORS cors: ConfigurationCORS = ConfigurationCORS() + + # KAFKA + kafka: ConfigurationKafka = ConfigurationKafka() settings = Setting() \ No newline at end of file diff --git a/gateway/core/logger.py b/gateway/core/logger.py index 5eb518b..1b6f971 100644 --- a/gateway/core/logger.py +++ b/gateway/core/logger.py @@ -54,6 +54,10 @@ def format(self, record): '()': 'core.logger.ServiceNameFilter', 'service_name': 'gateway_microservice_httpx' }, + 'kafka': { + '()': 'core.logger.ServiceNameFilter', + 'service_name': 'gateway_microservice_kafka' + }, }, 'handlers': { 'console': { @@ -91,5 +95,11 @@ def format(self, record): 'propagate': False, 'filters': ['httpx'], }, + 'kafka_logger': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + 'filters': ['kafka'], + }, }, } \ No newline at end of file diff --git a/gateway/infrastructure/external_service_facade.py b/gateway/infrastructure/external_service_facade.py index b4bdb1f..c4f55d5 100644 --- a/gateway/infrastructure/external_service_facade.py +++ b/gateway/infrastructure/external_service_facade.py @@ -41,7 +41,7 @@ async def proxy_endpoint( elif method == 'patch': response = await client.patch( f'{self.base_url}/api/v1/{endpoint}', - json=data.model_dump(), + json=data.model_dump(exclude_unset=True), follow_redirects=True ) elif method == 'delete': diff --git a/gateway/infrastructure/kafka/producer.py b/gateway/infrastructure/kafka/producer.py new file mode 100644 index 0000000..7978495 --- /dev/null +++ b/gateway/infrastructure/kafka/producer.py @@ -0,0 +1,38 @@ +import asyncio, json +from fastapi import FastAPI +from typing import AsyncGenerator +from contextlib import asynccontextmanager +from aiokafka import AIOKafkaProducer +from core.config import settings + + +async def _start_producer_with_retries(producer: AIOKafkaProducer): + last_exc = None + for i in range(1, settings.kafka.STARTUP_RETRIES + 1): + try: + await producer.start() + return + except Exception as exc: + last_exc = exc + await asyncio.sleep(settings.kafka.RETRY_BACKOFF * i) + raise last_exc + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + producer = AIOKafkaProducer( + bootstrap_servers=settings.kafka.BOOTSTRAP, + client_id=settings.kafka.CLIENT_ID, + value_serializer=lambda v: json.dumps(v).encode('utf-8'), + ) + # startup + await _start_producer_with_retries(producer) + app.state.kafka_producer = producer + app.state.kafka_producer_available = True + try: + yield + finally: + # shutdown + try: + await producer.stop() + except Exception: + pass \ No newline at end of file diff --git a/gateway/locustfile.py b/gateway/locustfile.py new file mode 100644 index 0000000..ba3016a --- /dev/null +++ b/gateway/locustfile.py @@ -0,0 +1,50 @@ +from locust import FastHttpUser, task, between +# from core.config import settings + +class UserBehavior(FastHttpUser): + wait_time = between(1, 2) + + @task(1) + def create_task_event(self): + self.client.post('/api/v1/task/create_event', json={ + 'title': 'Create Task', + 'description': None, + }) + + @task(2) + def create_task(self): + self.client.post('/api/v1/task/create', json={ + 'title': 'Create Task', + 'description': None, + }) + + @task(3) + def get_tasks_list(self): + self.client.get('/api/v1/tasks/') + +# class CreateTaskOnlyUser(FastHttpUser): +# wait_time = between(1, 2) + +# @task +# def create_task(self): +# self.client.post('/api/v1/task/create', json={ +# 'title': 'Create Task', +# 'description': None, +# }) + +# class ReadOnlyUser(FastHttpUser): +# wait_time = between(1, 2) + +# @task +# def get_tasks(self): +# self.client.get('/api/v1/tasks/') + +# class CreateTaskEventUser(FastHttpUser): +# wait_time = between(1, 2) + +# @task +# def create_task_event(self): +# self.client.post('/api/v1/task/create_event', json={ +# 'title': 'Create Task', +# 'description': None, +# }) \ No newline at end of file diff --git a/gateway/main.py b/gateway/main.py index 3cf58a5..0a1b22e 100644 --- a/gateway/main.py +++ b/gateway/main.py @@ -1,11 +1,14 @@ -import uvicorn - +import uvicorn, strawberry +from strawberry.fastapi import GraphQLRouter from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.openapi.docs import get_swagger_ui_html from core.config import settings from api_v1.rest import router as router_v1 +from infrastructure.kafka.producer import lifespan +from api_v1.graphql.tasks.resolvers import Query, Mutation + import logging.config from core.logger import logger_config @@ -16,8 +19,10 @@ title='API Gateway', description='API Gateway for microservices', version='1.0.0', + lifespan=lifespan, # docs_url=None, # redoc_url=None, + # openapi_url=None, ) app.mount('/static', StaticFiles(directory='static'), name='static') @@ -40,6 +45,16 @@ async def custom_swagger_ui_html(): app.include_router(router=router_v1, prefix=settings.api_v1_prefix) +schema = strawberry.Schema( + query=Query, + mutation=Mutation +) +graphql_app = GraphQLRouter(schema) +app.include_router( + router=graphql_app, + prefix=settings.api_v1_prefix + '/graphql', + include_in_schema=False +) if __name__ == '__main__': uvicorn.run('main:app', host='0.0.0.0', port=settings.api_v1_port, reload=True) \ No newline at end of file diff --git a/gateway/poetry.lock b/gateway/poetry.lock index 5f26799..bbf0044 100644 --- a/gateway/poetry.lock +++ b/gateway/poetry.lock @@ -1,5 +1,58 @@ # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +[[package]] +name = "aiokafka" +version = "0.12.0" +description = "Kafka integration with asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiokafka-0.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da8938eac2153ca767ac0144283b3df7e74bb4c0abc0c9a722f3ae63cfbf3a42"}, + {file = "aiokafka-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5c827c8883cfe64bc49100de82862225714e1853432df69aba99f135969bb1b"}, + {file = "aiokafka-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea5710f7707ed12a7f8661ab38dfa80f5253a405de5ba228f457cc30404eb51"}, + {file = "aiokafka-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87b1a45c57bbb1c17d1900a74739eada27e4f4a0b0932ab3c5a8cbae8bbfe1e"}, + {file = "aiokafka-0.12.0-cp310-cp310-win32.whl", hash = "sha256:1158e630664d9abc74d8a7673bc70dc10737ff758e1457bebc1c05890f29ce2c"}, + {file = "aiokafka-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:06f5889acf8e1a81d6e14adf035acb29afd1f5836447fa8fa23d3cbe8f7e8608"}, + {file = "aiokafka-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddc5308c43d48af883667e2f950a0a9739ce2c9bfe69a0b55dc234f58b1b42d6"}, + {file = "aiokafka-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff63689cafcd6dd642a15de75b7ae121071d6162cccba16d091bcb28b3886307"}, + {file = "aiokafka-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24633931e05a9dc80555a2f845572b6845d2dcb1af12de27837b8602b1b8bc74"}, + {file = "aiokafka-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b2436c7c69384d210e9169fbfe339d9f49dbdcfddd8d51c79b9877de545e33"}, + {file = "aiokafka-0.12.0-cp311-cp311-win32.whl", hash = "sha256:90511a2c4cf5f343fc2190575041fbc70171654ab0dae64b3bbabd012613bfa7"}, + {file = "aiokafka-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:04c8ad27d04d6c53a1859687015a5f4e58b1eb221e8a7342d6c6b04430def53e"}, + {file = "aiokafka-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b01947553ff1120fa1cb1a05f2c3e5aa47a5378c720bafd09e6630ba18af02aa"}, + {file = "aiokafka-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3c8ec1c0606fa645462c7353dc3e4119cade20c4656efa2031682ffaad361c0"}, + {file = "aiokafka-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577c1c48b240e9eba57b3d2d806fb3d023a575334fc3953f063179170cc8964f"}, + {file = "aiokafka-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b815b2e5fed9912f1231be6196547a367b9eb3380b487ff5942f0c73a3fb5c"}, + {file = "aiokafka-0.12.0-cp312-cp312-win32.whl", hash = "sha256:5a907abcdf02430df0829ac80f25b8bb849630300fa01365c76e0ae49306f512"}, + {file = "aiokafka-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:fdbd69ec70eea4a8dfaa5c35ff4852e90e1277fcc426b9380f0b499b77f13b16"}, + {file = "aiokafka-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f9e8ab97b935ca681a5f28cf22cf2b5112be86728876b3ec07e4ed5fc6c21f2d"}, + {file = "aiokafka-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed991c120fe19fd9439f564201dd746c4839700ef270dd4c3ee6d4895f64fe83"}, + {file = "aiokafka-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c01abf9787b1c3f3af779ad8e76d5b74903f590593bc26f33ed48750503e7f7"}, + {file = "aiokafka-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08c84b3894d97fd02fcc8886f394000d0f5ce771fab5c498ea2b0dd2f6b46d5b"}, + {file = "aiokafka-0.12.0-cp313-cp313-win32.whl", hash = "sha256:63875fed922c8c7cf470d9b2a82e1b76b4a1baf2ae62e07486cf516fd09ff8f2"}, + {file = "aiokafka-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:bdc0a83eb386d2384325d6571f8ef65b4cfa205f8d1c16d7863e8d10cacd995a"}, + {file = "aiokafka-0.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9590554fae68ec80099beae5366f2494130535a1a3db0c4fa5ccb08f37f6e46"}, + {file = "aiokafka-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c77f5953ff4b25c889aef26df1f28df66c58db7abb7f34ecbe48502e9a6d273"}, + {file = "aiokafka-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96d7fd8fdb5f439f7e7860fd8ec37870265d0578475e82049bce60ab07ca045"}, + {file = "aiokafka-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ddff02b1e981083dff6d1a80d4502e0e83e0e480faf1f881766ca6f23e8d22"}, + {file = "aiokafka-0.12.0-cp39-cp39-win32.whl", hash = "sha256:4aab2767dcc8923626d8d60c314f9ba633563249cff71750db5d70b6ec813da2"}, + {file = "aiokafka-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:7a57fda053acd1b88c87803ad0381a1d2a29d36ec561550d11ce9154972b8e23"}, + {file = "aiokafka-0.12.0.tar.gz", hash = "sha256:62423895b866f95b5ed8d88335295a37cc5403af64cb7cb0e234f88adc2dff94"}, +] + +[package.dependencies] +async-timeout = "*" +packaging = "*" +typing-extensions = ">=4.10.0" + +[package.extras] +all = ["cramjam (>=2.8.0)", "gssapi"] +gssapi = ["gssapi"] +lz4 = ["cramjam (>=2.8.0)"] +snappy = ["cramjam"] +zstd = ["cramjam"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -31,13 +84,383 @@ sniffio = ">=1.1" [package.extras] trio = ["trio (>=0.26.1)"] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "bidict" +version = "0.23.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, + {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\" or implementation_name == \"pypy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + [[package]] name = "click" version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -52,13 +475,29 @@ 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 = ["main"] +groups = ["main", "dev"] markers = "platform_system == \"Windows\" or 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 = "configargparse" +version = "1.7.1" +description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6"}, + {file = "configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9"}, +] + +[package.extras] +test = ["PyYAML", "mock", "pytest"] +yaml = ["PyYAML"] + [[package]] name = "fastapi" version = "0.116.1" @@ -81,13 +520,349 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (> standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "flask" +version = "3.1.2" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "5.0.0" +description = "A Flask extension adding a decorator for CORS support" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"}, + {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"}, +] + +[package.dependencies] +Flask = ">=0.9" + +[[package]] +name = "flask-login" +version = "0.6.3" +description = "User authentication and session management for Flask." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"}, + {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"}, +] + +[package.dependencies] +Flask = ">=1.0.4" +Werkzeug = ">=1.0.1" + +[[package]] +name = "gevent" +version = "25.5.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "gevent-25.5.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8e5a0fab5e245b15ec1005b3666b0a2e867c26f411c8fe66ae1afe07174a30e9"}, + {file = "gevent-25.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7b80a37f2fb45ee4a8f7e64b77dd8a842d364384046e394227b974a4e9c9a52"}, + {file = "gevent-25.5.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29ab729d50ae85077a68e0385f129f5b01052d01a0ae6d7fdc1824f5337905e4"}, + {file = "gevent-25.5.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80d20592aeabcc4e294fd441fd43d45cb537437fd642c374ea9d964622fad229"}, + {file = "gevent-25.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8ba0257542ccbb72a8229dc34d00844ccdfba110417e4b7b34599548d0e20e9"}, + {file = "gevent-25.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cad0821dff998c7c60dd238f92cd61380342c47fb9e92e1a8705d9b5ac7c16e8"}, + {file = "gevent-25.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:017a7384c0cd1a5907751c991535a0699596e89725468a7fc39228312e10efa1"}, + {file = "gevent-25.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:469c86d02fccad7e2a3d82fe22237e47ecb376fbf4710bc18747b49c50716817"}, + {file = "gevent-25.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:12380aba5c316e9ff53cc21d8ab80f4a91c0df3ada58f65d4f5eb2cf693db00e"}, + {file = "gevent-25.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f0694daab1a041b69a53f53c2141c12994892b2503870515cabe6a5dbd2a928"}, + {file = "gevent-25.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2797885e9aeffdc98e1846723e5aa212e7ce53007dbef40d6fd2add264235c41"}, + {file = "gevent-25.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cde6aaac36b54332e10ea2a5bc0de6a8aba6c205c92603fe4396e3777c88e05d"}, + {file = "gevent-25.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24484f80f14befb8822bf29554cfb3a26a26cb69cd1e5a8be9e23b4bd7a96e25"}, + {file = "gevent-25.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc7446895fa184890d8ca5ea61e502691114f9db55c9b76adc33f3086c4368"}, + {file = "gevent-25.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5b6106e2414b1797133786258fa1962a5e836480e4d5e861577f9fc63b673a5a"}, + {file = "gevent-25.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:bc899212d90f311784c58938a9c09c59802fb6dc287a35fabdc36d180f57f575"}, + {file = "gevent-25.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d87c0a1bd809d8f70f96b9b229779ec6647339830b8888a192beed33ac8d129f"}, + {file = "gevent-25.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87a4b66edb3808d4d07bbdb0deed5a710cf3d3c531e082759afd283758bb649"}, + {file = "gevent-25.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f076779050029a82feb0cb1462021d3404d22f80fa76a181b1a7889cd4d6b519"}, + {file = "gevent-25.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb673eb291c19370f69295f7a881a536451408481e2e3deec3f41dedb7c281ec"}, + {file = "gevent-25.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1325ed44225c8309c0dd188bdbbbee79e1df8c11ceccac226b861c7d52e4837"}, + {file = "gevent-25.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fcd5bcad3102bde686d0adcc341fade6245186050ce14386d547ccab4bd54310"}, + {file = "gevent-25.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1a93062609e8fa67ec97cd5fb9206886774b2a09b24887f40148c9c37e6fb71c"}, + {file = "gevent-25.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:2534c23dc32bed62b659ed4fd9e198906179e68b26c9276a897e04163bdde806"}, + {file = "gevent-25.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a022a9de9275ce0b390b7315595454258c525dc8287a03f1a6cacc5878ab7cbc"}, + {file = "gevent-25.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fae8533f9d0ef3348a1f503edcfb531ef7a0236b57da1e24339aceb0ce52922"}, + {file = "gevent-25.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c7b32d9c3b5294b39ea9060e20c582e49e1ec81edbfeae6cf05f8ad0829cb13d"}, + {file = "gevent-25.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b95815fe44f318ebbfd733b6428b4cb18cc5e68f1c40e8501dd69cc1f42a83d"}, + {file = "gevent-25.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d316529b70d325b183b2f3f5cde958911ff7be12eb2b532b5c301f915dbbf1e"}, + {file = "gevent-25.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f6ba33c13db91ffdbb489a4f3d177a261ea1843923e1d68a5636c53fe98fa5ce"}, + {file = "gevent-25.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ee34b77c7553777c0b8379915f75934c3f9c8cd32f7cd098ea43c9323c2276"}, + {file = "gevent-25.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fa6aa0da224ed807d3b76cdb4ee8b54d4d4d5e018aed2478098e685baae7896"}, + {file = "gevent-25.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:0bacf89a65489d26c7087669af89938d5bfd9f7afb12a07b57855b9fad6ccbd0"}, + {file = "gevent-25.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30169ef9cc0a57930bfd8fe14d86bc9d39fb96d278e3891e85cbe7b46058a97"}, + {file = "gevent-25.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e72ad5f8d9c92df017fb91a1f6a438cfb63b0eff4b40904ff81b40cb8150078c"}, + {file = "gevent-25.5.1-cp39-cp39-win32.whl", hash = "sha256:e5f358e81e27b1a7f2fb2f5219794e13ab5f59ce05571aa3877cfac63adb97db"}, + {file = "gevent-25.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b83aff2441c7d4ee93e519989713b7c2607d4510abe990cd1d04f641bc6c03af"}, + {file = "gevent-25.5.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:60ad4ca9ca2c4cc8201b607c229cd17af749831e371d006d8a91303bb5568eb1"}, + {file = "gevent-25.5.1.tar.gz", hash = "sha256:582c948fa9a23188b890d0bc130734a506d039a2e5ad87dae276a456cc683e61"}, +] + +[package.dependencies] +cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""} +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"] + +[[package]] +name = "geventhttpclient" +version = "2.3.4" +description = "HTTP client library for gevent" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "geventhttpclient-2.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:182f5158504ac426d591cfb1234de5180813292b49049e761f00bf70691aace5"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59a2e7c136a3e6b60b87bf8b87e5f1fb25705d76ab7471018e25f8394c640dda"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fde955b634a593e70eae9b4560b74badc8b2b1e3dd5b12a047de53f52a3964a"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c69c4ec9b618ca42008d6930077d72ee0c304e2272a39a046e775c25ca4ac44"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aaa7aebf4fe0d33a3f9f8945061f5374557c9f7baa3c636bfe25ac352167be9c"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:08ea2e92a1a4f46d3eeff631fa3f04f4d12c78523dc9bffc3b05b3dd93233050"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49f5e2051f7d06cb6476500a2ec1b9737aa3160258f0344b07b6d8e8cda3a0cb"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0599fd7ca84a8621f8d34c4e2b89babae633b34c303607c61500ebd3b8a7687a"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4ac86f8d4ddd112bd63aa9f3c7b73c62d16b33fca414f809e8465bbed2580a3"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4b796a59bed199884fe9d59a447fd685aa275a1406bc1f7caebd39a257f56e"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:650bf5d07f828a0cb173dacc4bb28e2ae54fd840656b3e552e5c3a4f96e29f08"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e16113d80bc270c465590ba297d4be8f26906ca8ae8419dc86520982c4099036"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:be2ade1516fdc7b7fb3d73e6f8d8bf2ce5b4e2e0933a5465a86d40dfa1423488"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:07152cad33b39d365f239b4fa1f818f4801c07e16ce0a0fee7d5fee2cabcb07b"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-win32.whl", hash = "sha256:c9d83bf2c274aed601e8b5320789e54661c240a831533e73a290da27d1c046f1"}, + {file = "geventhttpclient-2.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:30671bb44f5613177fc1dc7c8840574d91ccd126793cd40fc16915a4abc67034"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fb8f6a18f1b5e37724111abbd3edf25f8f00e43dc261b11b10686e17688d2405"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dbb28455bb5d82ca3024f9eb7d65c8ff6707394b584519def497b5eb9e5b1222"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96578fc4a5707b5535d1c25a89e72583e02aafe64d14f3b4d78f9c512c6d613c"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19721357db976149ccf54ac279eab8139da8cdf7a11343fd02212891b6f39677"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecf830cdcd1d4d28463c8e0c48f7f5fb06f3c952fff875da279385554d1d4d65"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47dbf8a163a07f83b38b0f8a35b85e5d193d3af4522ab8a5bbecffff1a4cd462"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e39ad577b33a5be33b47bff7c2dda9b19ced4773d169d6555777cd8445c13c0"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:110d863baf7f0a369b6c22be547c5582e87eea70ddda41894715c870b2e82eb0"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d9fca98469bd770e3efd88326854296d1aa68016f285bd1a2fb6cd21e17ee"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71dbc6d4004017ef88c70229809df4ad2317aad4876870c0b6bcd4d6695b7a8d"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ed35391ad697d6cda43c94087f59310f028c3e9fb229e435281a92509469c627"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:97cd2ab03d303fd57dea4f6d9c2ab23b7193846f1b3bbb4c80b315ebb5fc8527"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec4d1aa08569b7eb075942caeacabefee469a0e283c96c7aac0226d5e7598fe8"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93926aacdb0f4289b558f213bc32c03578f3432a18b09e4b6d73a716839d7a74"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ea87c25e933991366049a42c88e91ad20c2b72e11c7bd38ef68f80486ab63cb2"}, + {file = "geventhttpclient-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:e02e0e9ef2e45475cf33816c8fb2e24595650bcf259e7b15b515a7b49cae1ccf"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ac30c38d86d888b42bb2ab2738ab9881199609e9fa9a153eb0c66fc9188c6cb"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b802000a4fad80fa57e895009671d6e8af56777e3adf0d8aee0807e96188fd9"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:461e4d9f4caee481788ec95ac64e0a4a087c1964ddbfae9b6f2dc51715ba706c"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7e41687c74e8fbe6a665458bbaea0c5a75342a95e2583738364a73bcbf1671b"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ea5da20f4023cf40207ce15f5f4028377ffffdba3adfb60b4c8f34925fce79"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91f19a8a6899c27867dbdace9500f337d3e891a610708e86078915f1d779bf53"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41f2dcc0805551ea9d49f9392c3b9296505a89b9387417b148655d0d8251b36e"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62f3a29bf242ecca6360d497304900683fd8f42cbf1de8d0546c871819251dad"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8714a3f2c093aeda3ffdb14c03571d349cb3ed1b8b461d9f321890659f4a5dbf"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b11f38b74bab75282db66226197024a731250dcbe25542fd4e85ac5313547332"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fccc2023a89dfbce2e1b1409b967011e45d41808df81b7fa0259397db79ba647"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9d54b8e9a44890159ae36ba4ae44efd8bb79ff519055137a340d357538a68aa3"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:407cb68a3c3a2c4f5d503930298f2b26ae68137d520e8846d8e230a9981d9334"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54fbbcca2dcf06f12a337dd8f98417a09a49aa9d9706aa530fc93acb59b7d83c"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-win32.whl", hash = "sha256:83143b41bde2eb010c7056f142cb764cfbf77f16bf78bda2323a160767455cf5"}, + {file = "geventhttpclient-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:46eda9a9137b0ca7886369b40995d2a43a5dff033d0a839a54241015d1845d41"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:be64c5583884c407fc748dedbcb083475d5b138afb23c6bc0836cbad228402cc"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:15b2567137734183efda18e4d6245b18772e648b6a25adea0eba8b3a8b0d17e8"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a4bca1151b8cd207eef6d5cb3c720c562b2aa7293cf113a68874e235cfa19c31"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8a681433e2f3d4b326d8b36b3e05b787b2c6dd2a5660a4a12527622278bf02ed"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:736aa8e9609e4da40aeff0dbc02fea69021a034f4ed1e99bf93fc2ca83027b64"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d477ae1f5d42e1ee6abbe520a2e9c7f369781c3b8ca111d1f5283c1453bc825"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b50d9daded5d36193d67e2fc30e59752262fcbbdc86e8222c7df6b93af0346a"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe705e7656bc6982a463a4ed7f9b1db8c78c08323f1d45d0d1d77063efa0ce96"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69668589359db4cbb9efa327dda5735d1e74145e6f0a9ffa50236d15cf904053"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ba526e07ccaf4f1c2cd3395dda221139f01468b6eee1190d4a616f187a0378"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:525bd192705b5cb41a7cc3fe41fca194bfd6b5b59997ab9fe68fe0a82dab6140"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:42b6f6afb0d3aab6a013c9cdb97e19bf4fe08695975670d0a018113d24cb344c"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:227579b703085c4e5c6d5217ad6565b19ac8d1164404133e5874efaae1905114"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d1d0db89c1c8f3282eac9a22fda2b4082e1ed62a2107f70e3f1de1872c7919f"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-win32.whl", hash = "sha256:4e492b9ab880f98f8a9cc143b96ea72e860946eae8ad5fb2837cede2a8f45154"}, + {file = "geventhttpclient-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:72575c5b502bf26ececccb905e4e028bb922f542946be701923e726acf305eb6"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:503db5dd0aa94d899c853b37e1853390c48c7035132f39a0bab44cbf95d29101"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:389d3f83316220cfa2010f41401c140215a58ddba548222e7122b2161e25e391"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20c65d404fa42c95f6682831465467dff317004e53602c01f01fbd5ba1e56628"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2574ee47ff6f379e9ef124e2355b23060b81629f1866013aa975ba35df0ed60b"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecf1b735591fb21ea124a374c207104a491ad0d772709845a10d5faa07fa833"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44e9ba810c28f9635e5c4c9cf98fc6470bad5a3620d8045d08693f7489493a3c"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:501d5c69adecd5eaee3c22302006f6c16aa114139640873b72732aa17dab9ee7"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:709f557138fb84ed32703d42da68f786459dab77ff2c23524538f2e26878d154"}, + {file = "geventhttpclient-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8b86815a30e026c6677b89a5a21ba5fd7b69accf8f0e9b83bac123e4e9f3b31"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:4371b1b1afc072ad2b0ff5a8929d73ffd86d582908d3e9e8d7911dc027b1b3a6"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6409fcda1f40d66eab48afc218b4c41e45a95c173738d10c50bc69c7de4261b9"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:142870c2efb6bd0a593dcd75b83defb58aeb72ceaec4c23186785790bd44a311"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a74f7b926badb3b1d47ea987779cb83523a406e89203070b58b20cf95d6f535"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a8cde016e5ea6eb289c039b6af8dcef6c3ee77f5d753e57b48fe2555cdeacca"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5aa16f2939a508667093b18e47919376f7db9a9acbe858343173c5a58e347869"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ffe87eb7f1956357c2144a56814b5ffc927cbb8932f143a0351c78b93129ebbc"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5ee758e37215da9519cea53105b2a078d8bc0a32603eef2a1f9ab551e3767dee"}, + {file = "geventhttpclient-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:416cc70adb3d34759e782d2e120b4432752399b85ac9758932ecd12274a104c3"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2fa223034774573218bb49e78eca7e92b8c82ccae9d840fdcf424ea95c2d1790"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f707dbdaad78dafe6444ee0977cbbaefa16ad10ab290d75709170d124bac4c8"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5660dfd692bc2cbd3bd2d0a2ad2a58ec47f7778042369340bdea765dc10e5672"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a85c0cdf16559c9cfa3e2145c16bfe5e1c3115d0cb3b143d41fb68412888171f"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:024b9e2e3203cc5e2c34cb5efd16ba0f2851e39c45abdc2966a8c30a935094fc"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d693d1f63ae6a794074ec1f475e3e3f607c52242f3799479fc483207b5c02ff0"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c7a0c11afc1fe2c8338e5ccfd7ffdab063b84ace8b9656b5b3bc1614ee8a234"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39746bcd874cb75aaf6d16cdddd287a29721e8b56c20dd8a4d4ecde1d3b92f14"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73e7d2e3d2d67e25d9d0f2bf46768650a57306a0587bbcdbfe2f4eac504248d2"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:063991edd5468401377116cc2a71361a88abce9951f60ba15b7fe1e10ce00f25"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d1e73172fed40c1d0e4f79fd15d357ead2161371b2ecdc82d626f143c29c8175"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:04a3328e687c419f78926a791df48c7672e724fa75002f2d3593df96510696e6"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2335963f883a94f503b321f7abfb38a4efbca70f9453c5c918cca40a844280cd"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e657db5a8c9498dee394db1e12085eda4b9cf7b682466364aae52765b930a884"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-win32.whl", hash = "sha256:be593e78cf4a7cbdbe361823fb35e1e0963d1a490cf90c8b6c680a30114b1a10"}, + {file = "geventhttpclient-2.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:88b5e6cc958907dd6a13d3f8179683c275f57142de95d0d652a54c8275e03a8b"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9f5514890bbb54a7c35fb66120c7659040182d54e735fe717642b67340b8131a"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4c24db3faa829244ded6805b47aec408df2f5b15fe681e957c61543070f6e405"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:195e396c59f25958ad6f79d2c58431cb8b1ff39b5821e6507bf539c79b5681dc"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c87a1762aba525b00aac34e1ffb97d083f94ef505282a461147298f32b2ae27"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75585278b2e3cd1a866bc2a95be7e0ab53c51c35c9e0e75161ff4f30817b3da8"}, + {file = "geventhttpclient-2.3.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fad0666d34122b5ad6de2715c0597b23eab523cc57caf38294138249805da15f"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:707a66cd1e3bf06e2c4f8f21d3b4e6290c9e092456f489c560345a8663cdd93e"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0129ce7ef50e67d66ea5de44d89a3998ab778a4db98093d943d6855323646fa5"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac2635f68b3b6752c2a576833d9d18f0af50bdd4bd7dd2d2ca753e3b8add84c"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71206ab89abdd0bd5fee21e04a3995ec1f7d8ae1478ee5868f9e16e85a831653"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8bde667d0ce46065fe57f8ff24b2e94f620a5747378c97314dcfc8fbab35b73"}, + {file = "geventhttpclient-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5f71c75fc138331cbbe668a08951d36b641d2c26fb3677d7e497afb8419538db"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1d23fe37b9d79b17dbce2d086006950d4527a2f95286046b7229e1bd3d8ac5e4"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:888e34d2e53d0f1dab85ff3e5ca81b8b7949b9e4702439f66f4ebf61189eb923"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a88925055acc56811927614bb8be3e784fdd5149819fa26c2af6a43a2e43f5"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ba0aa08f5eaa7165bf90fb06adf124511dbdf517500ab0793883f648feaaf8"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9db12e764ec1a4648d67b1501f7001e30f92e05a1692a75920ab53670c4958b"}, + {file = "geventhttpclient-2.3.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e310f6313ccba476dc1f393fd40738ca3b7fa3bb41c31c38f9641b1927306ba2"}, + {file = "geventhttpclient-2.3.4.tar.gz", hash = "sha256:1749f75810435a001fc6d4d7526c92cf02b39b30ab6217a886102f941c874222"}, +] + +[package.dependencies] +brotli = "*" +certifi = "*" +gevent = "*" +urllib3 = "*" + +[package.extras] +benchmarks = ["httplib2", "httpx", "requests", "urllib3"] +dev = ["dpkt", "pytest", "requests"] +examples = ["oauth2"] + +[[package]] +name = "graphql-core" +version = "3.2.6" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +optional = false +python-versions = "<4,>=3.6" +groups = ["main"] +files = [ + {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, + {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\"" +files = [ + {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, + {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, + {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, + {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, + {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, + {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, + {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, + {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + [[package]] name = "h11" version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -155,7 +930,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -164,6 +939,451 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lia-web" +version = "0.2.3" +description = "A library for working with web frameworks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "lia_web-0.2.3-py3-none-any.whl", hash = "sha256:237c779c943cd4341527fc0adfcc3d8068f992ee051f4ef059b8474ee087f641"}, + {file = "lia_web-0.2.3.tar.gz", hash = "sha256:ccc9d24cdc200806ea96a20b22fb68f4759e6becdb901bd36024df7921e848d7"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.0" + +[[package]] +name = "libcst" +version = "1.8.4" +description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "libcst-1.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:114343271f70a79e6d08bc395f5dfa150227341fab646cc0a58e80550e7659b7"}, + {file = "libcst-1.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b376ef7fa30bef611d4fb32af1da0e767b801b00322028a874ab3a441686b6a9"}, + {file = "libcst-1.8.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9be5b1b7d416900ff9bcdb4945692e6252fdcbd95514e98439f81568568c9e02"}, + {file = "libcst-1.8.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e4c5055e255d12745c7cc60fb5fb31c0f82855864c15dc9ad33a44f829b92600"}, + {file = "libcst-1.8.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b1e570ba816da408b5ee40ac479b34e56d995bf32dcca6f0ddb3d69b08e77de"}, + {file = "libcst-1.8.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65364c214251ed5720f3f6d0c4ef1338aac91ad4bbc5d30253eac21832b0943a"}, + {file = "libcst-1.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:a90c80e4d89222e11c7a734bc1b7f930bc2aba7750ad149bde1b136f839ea788"}, + {file = "libcst-1.8.4-cp310-cp310-win_arm64.whl", hash = "sha256:2d71e7e5982776f78cca9102286bb0895ef6f7083f76c0c9bc5ba4e9e40aee38"}, + {file = "libcst-1.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e7baaa6f01b6b6ea4b28d60204fddc679a3cd56d312beee200bd5f8f9711f0b"}, + {file = "libcst-1.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:259737faf90552a0589d95393dcaa3d3028be03ab3ea87478d46a1a4f922dd91"}, + {file = "libcst-1.8.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a65e3c409ef16ae369600d085d23a3897d4fccf4fdcc09294a402c513ac35906"}, + {file = "libcst-1.8.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fa870f34018c7241ee9227723cac0787599a2a8a2bfd53eacfbbe1ea1a272ae6"}, + {file = "libcst-1.8.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3eeba4edb40b2291c2460fe8d7e43f47e5fcc33f186675db5d364395adca3401"}, + {file = "libcst-1.8.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a5cd7beef667e5de3c5fb0ec387dc19aeda5cd4606ff541d0e8613bb3ef3b23"}, + {file = "libcst-1.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:3de575f0b5b466f2e9656b963f5848103cc518c6f3581902c6f430b07864584f"}, + {file = "libcst-1.8.4-cp311-cp311-win_arm64.whl", hash = "sha256:2fcff2130824f2cb5f4fd9c4c74fb639c5f02bc4228654461f6dc6b1006f20c0"}, + {file = "libcst-1.8.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1d468514a21cf3444dc3f3a4b1effc6c05255c98cc79e02af394652d260139f0"}, + {file = "libcst-1.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:870a49df8575c11ea4f5319d54750f95d2d06370a263bd42d924a9cf23cf0cbe"}, + {file = "libcst-1.8.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c9c775bc473225a0ad8422150fd9cf18ed2eebd7040996772937ac558f294d6c"}, + {file = "libcst-1.8.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27eeb16edb7dc0711d67e28bb8c0288e4147210aeb2434f08c16ac5db6b559e5"}, + {file = "libcst-1.8.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e12101ef2a6e05b7610badb2bfa597379289f1408e305a8d19faacdb872f47"}, + {file = "libcst-1.8.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:69b672c1afac5fe00d689f585ba57ac5facc4632f39b977d4b3e4711571c76e2"}, + {file = "libcst-1.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7832ee448fbdf18884a1f9af5fba1be6d5e98deb560514d92339fd6318aef651"}, + {file = "libcst-1.8.4-cp312-cp312-win_arm64.whl", hash = "sha256:6840e4011b583e9b7a71c00e7ab4281aea7456877b3ea6ecedb68a39a000bc64"}, + {file = "libcst-1.8.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e8d5158f976a5ee140ad0d3391e1a1b84b2ce5da62f16e48feab4bc21b91967"}, + {file = "libcst-1.8.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a179c712f38acb85e81d8949e80e05a422c92dcf5a00d8f4976f7e547a9f0916"}, + {file = "libcst-1.8.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d130f3e2d40c5f48cbbc804710ddf5b4db9dd7c0118f3b35f109164a555860d2"}, + {file = "libcst-1.8.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a4270123c988e130cec94bfe1b54d34784a40b34b2d5ac0507720c1272bd3209"}, + {file = "libcst-1.8.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea74c56cb11a1fdca9f8ab258965adce23e049ef525fdcc5c254a093e3de25cb"}, + {file = "libcst-1.8.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7fe97d432d95b6bcb1694a6d0fa7e07dde8fa687a637958126410ee2ced94b81"}, + {file = "libcst-1.8.4-cp313-cp313-win_amd64.whl", hash = "sha256:2c6d8f7087e9eaf005efde573f3f36d1d40366160155c195a6c4230d4c8a5839"}, + {file = "libcst-1.8.4-cp313-cp313-win_arm64.whl", hash = "sha256:062e424042c36a102abd11d8e9e27ac6be68e1a934b0ecfc9fb8fea017240d2f"}, + {file = "libcst-1.8.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:873dd4e8b896f7cb0e78118badda55ec1f42e9301a4a948cc438955ff3ae2257"}, + {file = "libcst-1.8.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:52c9376ba11ede5430e40aa205101dfc41202465103c6540f24591f898afb3d6"}, + {file = "libcst-1.8.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:074a3b17e270237fb36d3b94d7492fb137cb74217674484ba25e015e8d3d8bdc"}, + {file = "libcst-1.8.4-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:846aad04bac624a42d182add526d019e417e6a2b8a4c0bf690d32f9e1f3075ff"}, + {file = "libcst-1.8.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:93c76ab41d736b66d6fb3df32cd33184eed17666d7dc3ce047cf7ccdfe80b5b1"}, + {file = "libcst-1.8.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f167bf83dce662c9b499f1ea078ec2f2fee138e80f7d7dbd59c89ed28dc935f"}, + {file = "libcst-1.8.4-cp313-cp313t-win_amd64.whl", hash = "sha256:43cbb6b41bc2c4785136f59a66692287d527aeb022789c4af44ad6e85b7b2baa"}, + {file = "libcst-1.8.4-cp313-cp313t-win_arm64.whl", hash = "sha256:6cc8b7e33f6c4677e220dd7025e1e980da4d3f497b9b8ee0320e36dd54597f68"}, + {file = "libcst-1.8.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d011d731c2e673fbd9c84794418230a913ae3c98fc86f27814612b6b6d53d26b"}, + {file = "libcst-1.8.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a334dd11cdea34275df91c2ae9cc5933ec7e0ad5698264966708d637d110b627"}, + {file = "libcst-1.8.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:783f52b7c8d82046f0d93812f62a25eb82c3834f198e6cbfd5bb03ca68b593c8"}, + {file = "libcst-1.8.4-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0352c7d662c89243e730a28edf41577f87e28649c18ee365dd373c5fbdab2434"}, + {file = "libcst-1.8.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb188ebd4114144e14f6beb5499e43bebd0ca3ce7f2beb20921d49138c67b814"}, + {file = "libcst-1.8.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a718e5f6b398a07ca5d533e6593c1590d69fe65c539323281959733d6d541dd"}, + {file = "libcst-1.8.4-cp314-cp314-win_amd64.whl", hash = "sha256:fedfd33e5dda2200d582554e6476626d4706aa1fa2794bfb271879f8edff89b9"}, + {file = "libcst-1.8.4-cp314-cp314-win_arm64.whl", hash = "sha256:eff724c17df10e059915000eaf59f4e79998b66a7d35681e934a9a48667df931"}, + {file = "libcst-1.8.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:64cc34d74c9543b30ec3d7481dd644cb1bb3888076b486592d7fa0f22632f1c6"}, + {file = "libcst-1.8.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3ad7f0a32ddcdff00a3eddfd35cfd8485d9f357a32e4c67558476570199f808f"}, + {file = "libcst-1.8.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2e156760fc741bbf2fa68f4e3b15f019e924ea852f02276d0a53b7375cf70445"}, + {file = "libcst-1.8.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:fceb17616f1afe528c88243e3e7f78f84f0cc287463f04f3c1243e20a469e869"}, + {file = "libcst-1.8.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5db0b484670aac7ea442213afaa9addb1de0d9540a34ad44d376bec12242bc3a"}, + {file = "libcst-1.8.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:929798ca38ea76a5056f725221d66c6923e749caa9fa7f4cc86e914a3698493d"}, + {file = "libcst-1.8.4-cp314-cp314t-win_amd64.whl", hash = "sha256:e6f309c0f42e323c527d8c9007f583fd1668e45884208184a70644d916f27829"}, + {file = "libcst-1.8.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4b1cbadd988fee59b25ea154708cfed99cfaf45f9685707be422ad736371a9fe"}, + {file = "libcst-1.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fbadca1bc31f696875c955080c407a40b2d1aa7f79ca174a65dcb0542a57db6c"}, + {file = "libcst-1.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d4111f971632e9ddf8191aeef4576595e18ef3fa7b3016bfe15a08fa8554df"}, + {file = "libcst-1.8.4-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f5bd0bcdd2a8da9dad47d36d71757d8ba87baf887ae6982e2cb8621846610c49"}, + {file = "libcst-1.8.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2e24d11a1be0b1791f7bace9d406f5a70b8691ef77be377b606950803de4657d"}, + {file = "libcst-1.8.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:14bda1e4ea0b04d3926d41f6dafbfd311a951b75a60fe0d79bb5a8249c1cef5b"}, + {file = "libcst-1.8.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:056733760ba5ac1fd4cd518cddd5a43b3adbe2e0f6c7ce02532a114f7cd5d85b"}, + {file = "libcst-1.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:33664117fcb2913fdbd7de07a009193b660a16e7af18f7c1b4449e428f3b0f95"}, + {file = "libcst-1.8.4-cp39-cp39-win_arm64.whl", hash = "sha256:b69e94625702825309fd9e50760e77a5a60bd1e7a8e039862c8dd3011a6e1530"}, + {file = "libcst-1.8.4.tar.gz", hash = "sha256:f0f105d32c49baf712df2be360d496de67a2375bcf4e9707e643b7efc2f9a55a"}, +] + +[package.dependencies] +pyyaml-ft = {version = ">=8.0.0", markers = "python_version >= \"3.13\""} + +[[package]] +name = "locust" +version = "2.40.5" +description = "Developer-friendly load testing framework" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "locust-2.40.5-py3-none-any.whl", hash = "sha256:c44a6c415c5218824895bd652a182a958c27a2ceb8427c835d2f4b90d735579b"}, + {file = "locust-2.40.5.tar.gz", hash = "sha256:4332f03ebfac83c115763e462f22f495783a88f1d237ccbd618d5b27863f5053"}, +] + +[package.dependencies] +configargparse = ">=1.7.1" +flask = ">=2.0.0" +flask-cors = ">=3.0.10" +flask-login = ">=0.6.3" +gevent = ">=24.10.1,<25.8.1" +geventhttpclient = ">=2.3.1" +locust-cloud = ">=1.27.0" +msgpack = ">=1.0.0" +psutil = ">=5.9.1" +pytest = ">=8.3.3,<9.0.0" +python-engineio = ">=4.12.2" +python-socketio = {version = ">=5.13.0", extras = ["client"]} +pywin32 = {version = "*", markers = "sys_platform == \"win32\""} +pyzmq = ">=25.0.0" +requests = ">=2.32.2" +setuptools = ">=70.0.0" +werkzeug = ">=2.0.0" + +[package.extras] +milvus = ["pymilvus (>=2.5.0)"] + +[[package]] +name = "locust-cloud" +version = "1.27.0" +description = "Locust Cloud" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "locust_cloud-1.27.0-py3-none-any.whl", hash = "sha256:0ddf732c1702d1d29f8359e261e23147d1bba373ac96c9125f80c290c2dcd9c1"}, + {file = "locust_cloud-1.27.0.tar.gz", hash = "sha256:b371a6940d978bb221ada9780e796e10e3032ff49ffeacf02c515aa876679b75"}, +] + +[package.dependencies] +configargparse = ">=1.7.1" +gevent = ">=24.10.1,<26.0.0" +platformdirs = ">=4.3.6,<5.0.0" +python-engineio = ">=4.12.2" +python-socketio = {version = "5.13.0", extras = ["client"]} + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.1" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, + {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, + {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, + {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, + {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, + {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, + {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, + {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, + {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, + {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, + {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, + {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, + {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, + {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[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 = "psutil" +version = "7.1.0" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13"}, + {file = "psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5"}, + {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3"}, + {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3"}, + {file = "psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d"}, + {file = "psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca"}, + {file = "psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d"}, + {file = "psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07"}, + {file = "psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] +test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and implementation_name != \"PyPy\" or implementation_name == \"pypy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -322,6 +1542,58 @@ 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.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[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 = "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 = ["main"] +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.1.1" @@ -337,6 +1609,92 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-engineio" +version = "4.12.2" +description = "Engine.IO server and client for Python" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f"}, + {file = "python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa"}, +] + +[package.dependencies] +simple-websocket = ">=0.10.0" + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +docs = ["sphinx"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "python-socketio" +version = "5.13.0" +description = "Socket.IO server and client for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf"}, + {file = "python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029"}, +] + +[package.dependencies] +bidict = ">=0.21.0" +python-engineio = ">=4.11.0" +requests = {version = ">=2.21.0", optional = true, markers = "extra == \"client\""} +websocket-client = {version = ">=0.54.0", optional = true, markers = "extra == \"client\""} + +[package.extras] +asyncio-client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] +docs = ["sphinx"] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -400,6 +1758,243 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +description = "YAML parser and emitter for Python with support for free-threading" +optional = false +python-versions = ">=3.13" +groups = ["main"] +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, + {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, + {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, + {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, + {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, + {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, + {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, + {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, + {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, + {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, + {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, + {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, + {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, + {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, + {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, + {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, + {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, + {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, + {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, + {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, + {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, + {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, + {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, + {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, + {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, + {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, + {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, + {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, + {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, + {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, + {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, + {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, + {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, + {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, + {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, + {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, + {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, + {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, + {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +description = "Simple WebSocket server and client for Python" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, + {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, +] + +[package.dependencies] +wsproto = "*" + +[package.extras] +dev = ["flake8", "pytest", "pytest-cov", "tox"] +docs = ["sphinx"] + +[[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 = ["main"] +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 = "sniffio" version = "1.3.1" @@ -430,6 +2025,69 @@ anyio = ">=3.6.2,<5" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "strawberry-graphql" +version = "0.282.0" +description = "A library for creating GraphQL APIs" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "strawberry_graphql-0.282.0-py3-none-any.whl", hash = "sha256:6671cf90fd3630ae7d50aae2d8a06808ada6ce55d02ee01b7b7cba0026dfed18"}, + {file = "strawberry_graphql-0.282.0.tar.gz", hash = "sha256:f15610e4944210c15f83be814bfcd6ac1024b4293c18c9f4eee8345639795d1c"}, +] + +[package.dependencies] +graphql-core = ">=3.2.0,<3.4.0" +lia-web = ">=0.2.1" +libcst = {version = "*", optional = true, markers = "extra == \"debug-server\""} +packaging = ">=23" +pygments = {version = ">=2.3,<3.0", optional = true, markers = "extra == \"debug-server\""} +python-dateutil = ">=2.7,<3.0" +python-multipart = {version = ">=0.0.7", optional = true, markers = "extra == \"debug-server\""} +rich = {version = ">=12.0.0", optional = true, markers = "extra == \"debug-server\""} +starlette = {version = ">=0.18.0", optional = true, markers = "extra == \"debug-server\""} +typer = {version = ">=0.7.0", optional = true, markers = "extra == \"debug-server\""} +typing-extensions = ">=4.5.0" +uvicorn = {version = ">=0.11.6", optional = true, markers = "extra == \"debug-server\""} +websockets = {version = ">=15.0.1,<16", optional = true, markers = "extra == \"debug-server\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.7.4.post0,<4)"] +asgi = ["python-multipart (>=0.0.7)", "starlette (>=0.18.0)"] +chalice = ["chalice (>=1.22,<2.0)"] +channels = ["asgiref (>=3.2,<4.0)", "channels (>=3.0.5)"] +cli = ["libcst", "pygments (>=2.3,<3.0)", "rich (>=12.0.0)", "typer (>=0.7.0)"] +debug = ["libcst", "rich (>=12.0.0)"] +debug-server = ["libcst", "pygments (>=2.3,<3.0)", "python-multipart (>=0.0.7)", "rich (>=12.0.0)", "starlette (>=0.18.0)", "typer (>=0.7.0)", "uvicorn (>=0.11.6)", "websockets (>=15.0.1,<16)"] +django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] +fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] +flask = ["flask (>=1.1)"] +litestar = ["litestar (>=2) ; python_version ~= \"3.10\""] +opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] +pydantic = ["pydantic (>1.6.1)"] +pyinstrument = ["pyinstrument (>=4.0.0)"] +quart = ["quart (>=0.19.3)"] +sanic = ["sanic (>=20.12.2)"] + +[[package]] +name = "typer" +version = "0.19.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.19.1-py3-none-any.whl", hash = "sha256:914b2b39a1da4bafca5f30637ca26fa622a5bf9f515e5fdc772439f306d5682a"}, + {file = "typer-0.19.1.tar.gz", hash = "sha256:cb881433a4b15dacc875bb0583d1a61e78497806741f9aba792abcab390c03e6"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "typing-extensions" version = "4.15.0" @@ -457,6 +2115,24 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.35.0" @@ -655,6 +2331,23 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "websockets" version = "15.0.1" @@ -734,7 +2427,108 @@ files = [ {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "zope-event" +version = "6.0" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3"}, + {file = "zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92"}, +] + +[package.dependencies] +setuptools = ">=75.8.2" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner (>=6.4)"] + +[[package]] +name = "zope-interface" +version = "8.0" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "zope_interface-8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf4d6ba488a0fb560980b575244aa962a75e77b7c86984138b8d52bd4b5465f"}, + {file = "zope_interface-8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0caca2915522451e92c96c2aec404d2687e9c5cb856766940319b3973f62abb8"}, + {file = "zope_interface-8.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a26ae2fe77c58b4df8c39c2b7c3aadedfd44225a1b54a1d74837cd27057b2fc8"}, + {file = "zope_interface-8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:453d2c6668778b8d2215430ed61e04417386e51afb23637ef2e14972b047b700"}, + {file = "zope_interface-8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a2c107cc6dff954be25399cd81ddc390667f79af306802fc0c1de98614348b70"}, + {file = "zope_interface-8.0-cp310-cp310-win_amd64.whl", hash = "sha256:c23af5b4c4e332253d721ec1222c809ad27ceae382ad5b8ff22c4c4fb6eb8ed5"}, + {file = "zope_interface-8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec1da7b9156ae000cea2d19bad83ddb5c50252f9d7b186da276d17768c67a3cb"}, + {file = "zope_interface-8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:160ba50022b342451baf516de3e3a2cd2d8c8dbac216803889a5eefa67083688"}, + {file = "zope_interface-8.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:879bb5bf937cde4acd738264e87f03c7bf7d45478f7c8b9dc417182b13d81f6c"}, + {file = "zope_interface-8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fb931bf55c66a092c5fbfb82a0ff3cc3221149b185bde36f0afc48acb8dcd92"}, + {file = "zope_interface-8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1858d1e5bb2c5ae766890708184a603eb484bb7454e306e967932a9f3c558b07"}, + {file = "zope_interface-8.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e88c66ebedd1e839082f308b8372a50ef19423e01ee2e09600b80e765a10234"}, + {file = "zope_interface-8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b80447a3a5c7347f4ebf3e50de319c8d2a5dabd7de32f20899ac50fc275b145d"}, + {file = "zope_interface-8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67047a4470cb2fddb5ba5105b0160a1d1c30ce4b300cf264d0563136adac4eac"}, + {file = "zope_interface-8.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1bee9c1b42513148f98d3918affd829804a5c992c000c290dc805f25a75a6a3f"}, + {file = "zope_interface-8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:804ebacb2776eb89a57d9b5e9abec86930e0ee784a0005030801ae2f6c04d5d8"}, + {file = "zope_interface-8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c4d9d3982aaa88b177812cd911ceaf5ffee4829e86ab3273c89428f2c0c32cc4"}, + {file = "zope_interface-8.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea1f2e47bc0124a03ee1e5fb31aee5dfde876244bcc552b9e3eb20b041b350d7"}, + {file = "zope_interface-8.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:ee9ecad04269c2da4b1be403a47993981531ffd557064b870eab4094730e5062"}, + {file = "zope_interface-8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a9a8a71c38628af82a9ea1f7be58e5d19360a38067080c8896f6cbabe167e4f8"}, + {file = "zope_interface-8.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c0cc51ebd984945362fd3abdc1e140dbd837c3e3b680942b3fa24fe3aac26ef8"}, + {file = "zope_interface-8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:07405019f635a93b318807cb2ec7b05a5ef30f67cf913d11eb2f156ddbcead0d"}, + {file = "zope_interface-8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:450ab3357799eed6093f3a9f1fa22761b3a9de9ebaf57f416da2c9fb7122cdcb"}, + {file = "zope_interface-8.0-cp313-cp313-win_amd64.whl", hash = "sha256:e38bb30a58887d63b80b01115ab5e8be6158b44d00b67197186385ec7efe44c7"}, + {file = "zope_interface-8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:778458ea69413cf8131a3fcc6f0ea2792d07df605422fb03ad87daca3f8f78ce"}, + {file = "zope_interface-8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b207966f39c2e6fcfe9b68333acb7b19afd3fdda29eccc4643f8d52c180a3185"}, + {file = "zope_interface-8.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e3cf57f90a760c56c55668f650ba20c3444cde8332820db621c9a1aafc217471"}, + {file = "zope_interface-8.0-cp39-cp39-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5cffe23eb610e32a83283dde5413ab7a17938fa3fbd023ca3e529d724219deb0"}, + {file = "zope_interface-8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d639d5015c1753031e180b8ef81e72bb7d47b0aca0218694ad1f19b0a6c6b63"}, + {file = "zope_interface-8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dee2d1db1067e8a4b682dde7eb4bff21775412358e142f4f98c9066173f9dacd"}, + {file = "zope_interface-8.0.tar.gz", hash = "sha256:b14d5aac547e635af749ce20bf49a3f5f93b8a854d2a6b1e95d4d5e5dc618f7d"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] +test = ["coverage[toml]", "zope.event", "zope.testing"] +testing = ["coverage[toml]", "zope.event", "zope.testing"] + [metadata] lock-version = "2.1" -python-versions = ">=3.13" -content-hash = "4171e4dd667f9f92551ecd415982ffc2dd873250383ef093f63469bb65c6f73b" +python-versions = ">=3.13,<4.0" +content-hash = "a4c6e3f31e6039082c7c205b04d761b6aef7a942d4c49d2ce720464575bbece2" diff --git a/gateway/pyproject.toml b/gateway/pyproject.toml index 91c15bf..399ee25 100644 --- a/gateway/pyproject.toml +++ b/gateway/pyproject.toml @@ -2,19 +2,24 @@ name = "gateway" version = "0.1.0" description = "" -authors = [ - {name = "Powermacintosh",email = "ak.powermacintosh@gmail.com"} -] -license = {text = "MIT"} +authors = [{ name = "Powermacintosh", email = "ak.powermacintosh@gmail.com" }] +license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.13,<4.0" dependencies = [ "fastapi (>=0.116.1,<0.117.0)", "uvicorn[standard] (>=0.35.0,<0.36.0)", - "pydantic-settings (>=2.10.1,<3.0.0)" + "pydantic-settings (>=2.10.1,<3.0.0)", + "aiokafka (>=0.12.0,<0.13.0)", + "strawberry-graphql[debug-server] (>=0.282.0,<0.283.0)", ] +[tool.poetry] +package-mode = false [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +locust = "^2.40.5" diff --git a/gateway/static/graphql-interface.png b/gateway/static/graphql-interface.png new file mode 100644 index 0000000..cc9327e Binary files /dev/null and b/gateway/static/graphql-interface.png differ diff --git a/gateway/static/swagger-custom.png b/gateway/static/swagger-custom.png index c56eb41..4050f15 100644 Binary files a/gateway/static/swagger-custom.png and b/gateway/static/swagger-custom.png differ diff --git a/tasks/.env.docker b/tasks/.env.docker new file mode 100644 index 0000000..2cb2e98 --- /dev/null +++ b/tasks/.env.docker @@ -0,0 +1,11 @@ +MODE = PRODUCTION + +# Database +TASKS_DB_USER = postgres +TASKS_DB_PASS = postgres +TASKS_DB_HOST = db_tasks_app +TASKS_DB_PORT = 5432 +TASKS_DB_NAME = tasks_app + +# FastAPI +TASKS_APP_PORT = 5001 \ No newline at end of file diff --git a/tasks/.env.test b/tasks/.env.test index 1799237..35ca79e 100644 --- a/tasks/.env.test +++ b/tasks/.env.test @@ -8,4 +8,9 @@ TASKS_DB_PORT = 5432 TASKS_DB_NAME = tasks_app_test # FastAPI -TASKS_APP_PORT = 5001 \ No newline at end of file +TASKS_APP_PORT = 5001 + +# Kafka +KAFKA_BOOTSTRAP = kafka_test:9092 +KAFKA_TOPIC = task_events_test +KAFKA_CLIENT_ID = tasks_app_test diff --git a/tasks/api_v1/graphql/context.py b/tasks/api_v1/graphql/context.py new file mode 100644 index 0000000..4f9212a --- /dev/null +++ b/tasks/api_v1/graphql/context.py @@ -0,0 +1,34 @@ +from typing import AsyncIterator +from contextlib import asynccontextmanager +from fastapi import Request +from strawberry.fastapi import BaseContext +from infrastructure.database.uow import UnitOfWork, unit_of_work +from api_v1.service.task import TaskService + + +class GraphQLContext(BaseContext): + def __init__( + self, + request: Request, + uow: UnitOfWork, + task_service: TaskService, + ): + self.request = request + self.uow = uow + self.task_service = task_service + +@asynccontextmanager +async def get_context(request: Request) -> AsyncIterator[GraphQLContext]: + """Контекст для GraphQL запросов, использующий существующий Unit of Work""" + async with unit_of_work() as uow: + task_service = TaskService(uow) + yield GraphQLContext( + request=request, + uow=uow, + task_service=task_service, + ) + +# Сохранение межзапросного контекста для GraphQL +async def get_context_wrapper(request: Request): + async with get_context(request) as ctx: + yield ctx diff --git a/tasks/api_v1/graphql/tasks/resolvers.py b/tasks/api_v1/graphql/tasks/resolvers.py new file mode 100644 index 0000000..8b71fe4 --- /dev/null +++ b/tasks/api_v1/graphql/tasks/resolvers.py @@ -0,0 +1,118 @@ +import strawberry +# from graphql import GraphQLError +from .schemas import ( + TaskGQL, + TaskPageGQL, + TaskCreateInputGQL, + TaskUpdateInputGQL, + TaskStatusGQL, + map_to_task_gql +) +from typing import Optional +from core.schemas.tasks import ( + TaskCreate, + TaskUpdatePartial, +) + +@strawberry.type +class Query: + @strawberry.field + async def task( + self, + id: strawberry.ID, + info: strawberry.Info, + ) -> Optional[TaskGQL]: + task = await info.context.task_service.get_task(id) + if task is None: + return None + return map_to_task_gql(task) + + @strawberry.field + async def tasks( + self, + status: Optional[TaskStatusGQL] = None, + offset: int = 0, + limit: int = 10, + info: strawberry.Info = strawberry.UNSET, + ) -> TaskPageGQL: + task_service = info.context.task_service + """Получение списка задач с опциональным фильтром по статусу.""" + status_value = status.value if status else None + + tasks_data = await task_service.get_tasks( + page=(offset // limit) + 1, + limit=limit, + column_search='status' if status else None, + input_search=status_value + ) + + tasks = [map_to_task_gql(task) for task in tasks_data.tasks] + + return TaskPageGQL( + tasks=tasks, + total=tasks_data.total, + pages_count=tasks_data.pages_count + ) + +@strawberry.type +class Mutation: + @strawberry.mutation + async def create_task( + self, + input: TaskCreateInputGQL, + info: strawberry.Info, + ) -> TaskGQL: + task_data = TaskCreate( + title=input.title, + description=input.description + ) + task = await info.context.task_service.create_task(task_data) + return map_to_task_gql(task) + + @strawberry.mutation + async def update_task( + self, + id: strawberry.ID, + input: TaskUpdateInputGQL, + info: strawberry.Info, + ) -> Optional[TaskGQL]: + task = await info.context.task_service.get_task(id) + if task is None: + return None + updated_task = await info.context.task_service.update_task( + task, TaskUpdatePartial(**input.to_update_dict()), partial=True + ) + return map_to_task_gql(updated_task) if updated_task else None + + @strawberry.mutation + async def delete_task( + self, + id: strawberry.ID, + info: strawberry.Info, + ) -> bool: + task = await info.context.task_service.get_task(id) + if task is None: + return False + await info.context.task_service.delete_task(task) + return True + + + + +# Автоматический маппинг (если много моделей) +# def input_to_partial(input_obj, mapping: dict = None) -> dict: +# data = {} +# for field, value in vars(input_obj).items(): +# if value is not None: +# if mapping and field in mapping: +# data[field] = mapping[field](value) +# else: +# data[field] = value +# return data + +# clean_fields = input_to_partial( +# input, +# mapping={"status": map_to_task_status} +# ) +# updated_task_data = TaskUpdatePartial(**clean_fields) + diff --git a/tasks/api_v1/graphql/tasks/schemas.py b/tasks/api_v1/graphql/tasks/schemas.py new file mode 100644 index 0000000..f8fd2fd --- /dev/null +++ b/tasks/api_v1/graphql/tasks/schemas.py @@ -0,0 +1,58 @@ +import strawberry +from enum import Enum +from typing import Optional, List +from core.models.task import TaskStatus +from core.models import Task + + +@strawberry.enum +class TaskStatusGQL(str, Enum): + CREATED = 'created' + IN_PROGRESS = 'in_progress' + COMPLETED = 'completed' + +def map_to_task_status(status: TaskStatusGQL) -> TaskStatus: + """Карта GraphQL TaskStatusGQL на domain TaskStatus""" + return TaskStatus(status.value) + +@strawberry.type +class TaskGQL: + id: strawberry.ID + title: str + description: Optional[str] = None + status: TaskStatusGQL + +def map_to_task_gql(task: Task) -> TaskGQL: + """Карта базы данных на GraphQL""" + return TaskGQL( + id=str(task.id), + title=task.title, + description=task.description, + status=TaskStatusGQL(task.status.value) if task.status else None + ) + +@strawberry.type +class TaskPageGQL: + tasks: List[TaskGQL] + total: int + pages_count: int + +@strawberry.input +class TaskCreateInputGQL: + title: str + description: Optional[str] = None + +@strawberry.input +class TaskUpdateInputGQL: + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TaskStatusGQL] = None + + def to_update_dict(self) -> dict: + fields = { + 'title': self.title, + 'description': self.description, + 'status': map_to_task_status(self.status) if self.status is not None else None, + } + return {k: v for k, v in fields.items() if v is not None} + diff --git a/tasks/api_v1/rest/tasks/decorators.py b/tasks/api_v1/rest/tasks/decorators.py index 031b258..7dab62a 100644 --- a/tasks/api_v1/rest/tasks/decorators.py +++ b/tasks/api_v1/rest/tasks/decorators.py @@ -1,11 +1,11 @@ from functools import wraps from fastapi import HTTPException, status from typing import Callable, TypeVar, Any -from infrastructure.database.exceptions import ( - DatabaseIntegrityError, - DatabaseConnectionError, - DatabaseQueryError, - DatabaseTimeoutError +from sqlalchemy.exc import ( + SQLAlchemyError, + IntegrityError, + OperationalError, + TimeoutError ) from .exceptions import ( TaskNotFoundException @@ -25,38 +25,38 @@ def handle_errors(func: Callable[..., T]) -> Callable[..., T]: async def wrapper(*args: Any, **kwargs: Any) -> T: try: return await func(*args, **kwargs) - except DatabaseIntegrityError as e: - logger.exception('Ошибка целостности данных', exc_info=e) + except IntegrityError as e: + logger.debug('Ошибка целостности данных', exc_info=e) raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(e) or 'Конфликт данных' ) - except (DatabaseConnectionError, DatabaseTimeoutError) as e: - logger.exception('Сервис временно недоступен', exc_info=e) + except (OperationalError, TimeoutError) as e: + logger.debug('Сервис временно недоступен', exc_info=e) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail='Сервис временно недоступен' ) - except DatabaseQueryError as e: - logger.exception('Некорректный запрос', exc_info=e) + except SQLAlchemyError as e: + logger.debug('Некорректный запрос', exc_info=e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Некорректный запрос' ) except TaskNotFoundException as e: - logger.exception('Задача не найдена', exc_info=e) + logger.debug('Задача не найдена', exc_info=e) raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) except ValueError as e: - logger.exception('Некорректные данные', exc_info=e) + logger.debug('Некорректные данные', exc_info=e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) or 'Некорректные данные' ) except Exception as e: - logger.exception('Неожиданная ошибка', exc_info=e) + logger.debug('Неожиданная ошибка', exc_info=e) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail='Внутренняя ошибка сервера' diff --git a/tasks/api_v1/rest/tasks/dependencies.py b/tasks/api_v1/rest/tasks/dependencies.py index e26c881..0eee4b0 100644 --- a/tasks/api_v1/rest/tasks/dependencies.py +++ b/tasks/api_v1/rest/tasks/dependencies.py @@ -1,13 +1,22 @@ -from typing import Annotated +from typing import Annotated, AsyncIterator from fastapi import Path, Depends -from infrastructure.database.uow import UnitOfWork, get_uow -from .service import TaskService +from infrastructure.database.uow import UnitOfWork, unit_of_work +from api_v1.service.task import TaskService -from infrastructure.database.models import Task +from core.models import Task from .decorators import handle_errors from .exceptions import TaskNotFoundException +from aiokafka import AIOKafkaProducer +async def get_producer() -> AIOKafkaProducer: + from main import app + return app.state.kafka_producer + +async def get_uow() -> AsyncIterator[UnitOfWork]: + async with unit_of_work() as uow: + yield uow + def get_task_service(uow: UnitOfWork = Depends(get_uow)) -> TaskService: return TaskService(uow) diff --git a/tasks/api_v1/rest/tasks/views.py b/tasks/api_v1/rest/tasks/views.py index 0f2eec5..f197c79 100644 --- a/tasks/api_v1/rest/tasks/views.py +++ b/tasks/api_v1/rest/tasks/views.py @@ -1,14 +1,27 @@ from typing import Annotated from fastapi import APIRouter, Depends, status, Path -from .dependencies import get_task_service, task_by_id -from .service import TaskService -from .schemas import ( +from .dependencies import ( + get_task_service, + task_by_id, + AIOKafkaProducer, + get_producer +) +from api_v1.service.task import TaskService +from core.models import Task +from core.schemas.tasks import ( TaskCreate, SchemaTask, TaskUpdate, TaskUpdatePartial, TasksResponseSchema ) +from core.config import settings + +import logging.config +from core.logger import logger_config + +logging.config.dictConfig(logger_config) +kafka_logger = logging.getLogger('kafka_logger') router, router_list = APIRouter(tags=['Tasks']), APIRouter(tags=['Tasks']) @@ -54,7 +67,7 @@ async def get_task(task: SchemaTask = Depends(task_by_id)): @router.put('/{task_id}/', response_model=SchemaTask, status_code=status.HTTP_200_OK) async def update_task( task_update: TaskUpdate, - task: SchemaTask = Depends(task_by_id), + task: Task = Depends(task_by_id), service: TaskService = Depends(get_task_service), ): """ @@ -79,7 +92,7 @@ async def update_task( @router.patch('/{task_id}/', response_model=SchemaTask, status_code=status.HTTP_200_OK) async def update_partial_task( task_update: TaskUpdatePartial, - task: SchemaTask = Depends(task_by_id), + task: Task = Depends(task_by_id), service: TaskService = Depends(get_task_service), ): """ @@ -104,7 +117,7 @@ async def update_partial_task( @router.delete('/{task_id}/', status_code=status.HTTP_204_NO_CONTENT) async def delete_task( - task: SchemaTask = Depends(task_by_id), + task: Task = Depends(task_by_id), service: TaskService = Depends(get_task_service), ): """ @@ -158,4 +171,21 @@ async def get_list_tasks( limit=limit, column_search=column_search, input_search=input_search, - ) \ No newline at end of file + ) + +@router.post('/create_event', status_code=status.HTTP_201_CREATED) +async def send_task_creation_event( + task: TaskCreate, + producer: AIOKafkaProducer = Depends(get_producer) +): + event = { + 'event': 'TaskModuleCreation', + 'task': task.model_dump() + } + try: + await producer.send_and_wait(topic=settings.kafka.TOPIC, value=event) + kafka_logger.info('Сообщение успешно отправлено в topic task_events') + except Exception as e: + kafka_logger.exception('Ошибка отправки сообщения в topic task_events', exc_info=e) + raise HTTPException(status_code=503, detail=f'Ошибка отправки сообщения в topic task_events: {e}') + return {'status': 'published'} \ No newline at end of file diff --git a/tasks/api_v1/rest/tasks/service.py b/tasks/api_v1/service/task.py similarity index 92% rename from tasks/api_v1/rest/tasks/service.py rename to tasks/api_v1/service/task.py index 1d94d0e..9750f46 100644 --- a/tasks/api_v1/rest/tasks/service.py +++ b/tasks/api_v1/service/task.py @@ -1,5 +1,5 @@ -from infrastructure.database.models import Task -from .schemas import ( +from core.models import Task +from core.schemas.tasks import ( TaskCreate, SchemaTask, TaskUpdate, @@ -40,7 +40,7 @@ async def get_tasks( async def update_task( self, - task: SchemaTask, + task: Task, task_update: TaskUpdate | TaskUpdatePartial, partial: bool = False, ) -> Optional[Task]: @@ -52,7 +52,7 @@ async def update_task( async def delete_task( self, - task: SchemaTask, + task: Task, ) -> None: return await self.uow.tasks.delete_task(task) \ No newline at end of file diff --git a/tasks/core/config.py b/tasks/core/config.py index ff3741e..e93a0b8 100644 --- a/tasks/core/config.py +++ b/tasks/core/config.py @@ -4,14 +4,12 @@ from pydantic import BaseModel from pydantic_settings import BaseSettings +load_dotenv() class ConfigurationDB(BaseModel): ######################### # PostgreSQL database # ######################### - load_dotenv() - - MODE: str = os.getenv('MODE') DB_USER: str = os.getenv('TASKS_DB_USER') DB_PASS: str = os.getenv('TASKS_DB_PASS') @@ -47,8 +45,23 @@ class ConfigurationCORS(BaseModel): 'Authorization', 'Access-Control-Allow-Origin', ] - + +class ConfigurationKafka(BaseModel): + ######################### + # KAFKA # + ######################### + + BOOTSTRAP: str = os.getenv('KAFKA_BOOTSTRAP', 'kafka:9092') + TOPIC: str = os.getenv('KAFKA_TOPIC', 'task_events') + GROUP_ID: str = os.getenv('KAFKA_GROUP_ID', 'tasks_app_group') + CLIENT_ID: str = os.getenv('KAFKA_CLIENT_ID', 'tasks_app') + STARTUP_RETRIES: int = os.getenv('KAFKA_STARTUP_RETRIES', 3) + RETRY_BACKOFF: float = os.getenv('KAFKA_RETRY_BACKOFF', 1.0) + class Setting(BaseSettings): + # ENV + MODE: str = os.getenv('MODE', 'DEVELOPMENT') + # FASTAPI api_v1_prefix: str = '/api/v1' api_v1_port: int = os.getenv('TASKS_APP_PORT', 5001) @@ -58,6 +71,9 @@ class Setting(BaseSettings): # CORS cors: ConfigurationCORS = ConfigurationCORS() + + # KAFKA + kafka: ConfigurationKafka = ConfigurationKafka() settings = Setting() \ No newline at end of file diff --git a/tasks/core/logger.py b/tasks/core/logger.py index a9f928f..50ceb4f 100644 --- a/tasks/core/logger.py +++ b/tasks/core/logger.py @@ -61,6 +61,10 @@ def format(self, record): '()': 'core.logger.ServiceNameFilter', 'service_name': 'tasks_microservice_uvicorn' }, + 'kafka': { + '()': 'core.logger.ServiceNameFilter', + 'service_name': 'tasks_microservice_kafka' + }, }, 'handlers': { 'console': { @@ -110,5 +114,29 @@ def format(self, record): 'propagate': False, 'filters': ['uvicorn'], }, + 'kafka_logger': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + 'filters': ['kafka'], + }, + 'aiokafka.cluster': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + 'filters': ['kafka'], + }, + 'aiokafka.consumer.subscription_state': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + 'filters': ['kafka'], + }, + 'aiokafka.consumer.group_coordinator': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + 'filters': ['kafka'], + }, }, } \ No newline at end of file diff --git a/tasks/core/models/__init__.py b/tasks/core/models/__init__.py new file mode 100644 index 0000000..8aad515 --- /dev/null +++ b/tasks/core/models/__init__.py @@ -0,0 +1,7 @@ +__all__ = ( + 'Base', + 'Task', +) + +from .base import Base +from .task import Task \ No newline at end of file diff --git a/tasks/infrastructure/database/models/base.py b/tasks/core/models/base.py similarity index 100% rename from tasks/infrastructure/database/models/base.py rename to tasks/core/models/base.py diff --git a/tasks/infrastructure/database/models/task.py b/tasks/core/models/task.py similarity index 100% rename from tasks/infrastructure/database/models/task.py rename to tasks/core/models/task.py diff --git a/tasks/infrastructure/database/repositories/task.py b/tasks/core/repositories/task.py similarity index 86% rename from tasks/infrastructure/database/repositories/task.py rename to tasks/core/repositories/task.py index 20cb90d..c545d99 100644 --- a/tasks/infrastructure/database/repositories/task.py +++ b/tasks/core/repositories/task.py @@ -1,9 +1,9 @@ from sqlalchemy import select, update, func from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional -from infrastructure.database.models import Task -from infrastructure.database.models.task import TaskStatus -from api_v1.rest.tasks.schemas import ( +from core.models import Task +from core.models.task import TaskStatus +from core.schemas.tasks import ( TaskCreate, SchemaTask, TaskUpdate, @@ -46,14 +46,18 @@ async def get_tasks( elif column_search == 'status': if input_search.upper() == 'CREATED': - stmt = stmt.where(Task.status == TaskStatus.CREATED) + status_condition = Task.status == TaskStatus.CREATED elif input_search.upper() == 'IN_PROGRESS': - stmt = stmt.where(Task.status == TaskStatus.IN_PROGRESS) + status_condition = Task.status == TaskStatus.IN_PROGRESS elif input_search.upper() == 'COMPLETED': - stmt = stmt.where(Task.status == TaskStatus.COMPLETED) + status_condition = Task.status == TaskStatus.COMPLETED else: logger.exception('Неизвестный статус задачи: %s', input_search) raise ValueError(f'Неизвестный статус задачи: {input_search}') + + # Apply the same status filter to both queries + stmt = stmt.where(status_condition) + total_stmt = total_stmt.where(status_condition) total_result = await self.session.execute(total_stmt) total_tasks = total_result.scalar() or 0 @@ -91,7 +95,7 @@ async def create_task( async def update_task( self, - task: SchemaTask, + task: Task, task_update: TaskUpdate | TaskUpdatePartial, partial: bool = False, ) -> Optional[Task]: @@ -105,7 +109,7 @@ async def update_task( async def delete_task( self, - task: SchemaTask, + task: Task, ) -> None: await self.session.delete(task) await self.session.flush() diff --git a/tasks/api_v1/rest/tasks/schemas.py b/tasks/core/schemas/tasks.py similarity index 100% rename from tasks/api_v1/rest/tasks/schemas.py rename to tasks/core/schemas/tasks.py diff --git a/tasks/infrastructure/database/exceptions.py b/tasks/infrastructure/database/exceptions.py deleted file mode 100644 index 4c8e7a2..0000000 --- a/tasks/infrastructure/database/exceptions.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional, Type, TypeVar, Union -from sqlalchemy.exc import ( - SQLAlchemyError, - IntegrityError, - OperationalError, - TimeoutError -) - -import logging.config -from core.logger import logger_config - -logging.config.dictConfig(logger_config) -logger = logging.getLogger('database_logger') - -T = TypeVar('T', bound='DatabaseError') - - -class DatabaseError(Exception): - """Базовое исключение для ошибок БД""" - def __init__(self, message: str, original_exception: Optional[Exception] = None): - self.message = message - self.original_exception = original_exception - logger.exception(f'{self.__class__.__name__}: {message}', exc_info=original_exception) - super().__init__(self.message) - - @classmethod - def from_sqlalchemy( - cls: Type[T], - exc: Union[SQLAlchemyError, Exception], - context: Optional[str] = None - ) -> T: - """Создает соответствующее исключение из SQLAlchemy ошибки""" - error_message = str(exc) if str(exc) else 'Неизвестная ошибка базы данных' - context_part = f' в {context}' if context else '' - message = f'Ошибка базы данных{context_part}: {error_message}' - - if isinstance(exc, IntegrityError): - return DatabaseIntegrityError(message, exc) - elif isinstance(exc, OperationalError): - return DatabaseConnectionError(message, exc) - elif isinstance(exc, TimeoutError): - return DatabaseTimeoutError(message, exc) - elif isinstance(exc, SQLAlchemyError): - return DatabaseQueryError(message, exc) - else: - return DatabaseUnknownError(message, exc) - -class DatabaseIntegrityError(DatabaseError): - """Исключение для ошибок целостности БД (уникальность, внешние ключи)""" - def __init__(self, message: str = 'Ошибка целостности базы данных', original_exception: Optional[Exception] = None): - super().__init__(message, original_exception) - -class DatabaseConnectionError(DatabaseError): - """Исключение для ошибок подключения к БД""" - def __init__(self, message: str = 'Ошибка подключения к базе данных', original_exception: Optional[Exception] = None): - super().__init__(message, original_exception) - -class DatabaseQueryError(DatabaseError): - """Исключение для ошибок выполнения запросов""" - def __init__(self, message: str = 'Ошибка выполнения запроса', original_exception: Optional[Exception] = None): - super().__init__(message, original_exception) - -class DatabaseTimeoutError(DatabaseError): - """Исключение для таймаутов БД""" - def __init__(self, message: str = 'Ошибка таймаута базы данных', original_exception: Optional[Exception] = None): - super().__init__(message, original_exception) - -class DatabaseUnknownError(DatabaseError): - """Исключение для непредвиденных ошибок""" - def __init__(self, message: str = 'Непредвиденная ошибка', original_exception: Optional[Exception] = None): - super().__init__(message, original_exception) \ No newline at end of file diff --git a/tasks/infrastructure/database/models/__init__.py b/tasks/infrastructure/database/models/__init__.py deleted file mode 100644 index 85f7874..0000000 --- a/tasks/infrastructure/database/models/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -__all__ = ( - 'Base', - # 'db_fastapi_connect', - 'Task', -) - -from .base import Base -# from .db_connect import ( -# db_fastapi_connect, -# ) -from .task import Task \ No newline at end of file diff --git a/tasks/infrastructure/database/uow.py b/tasks/infrastructure/database/uow.py index b022e2d..faf6bfc 100644 --- a/tasks/infrastructure/database/uow.py +++ b/tasks/infrastructure/database/uow.py @@ -1,10 +1,21 @@ from contextlib import asynccontextmanager from typing import AsyncIterator -from .exceptions import DatabaseError from sqlalchemy.ext.asyncio import AsyncSession -from .repositories.task import TaskRepository +from core.repositories.task import TaskRepository from .db_connect import async_session +from sqlalchemy.exc import ( + SQLAlchemyError, + IntegrityError, + OperationalError, + TimeoutError +) +from fastapi import HTTPException +import logging.config +from core.logger import logger_config + +logging.config.dictConfig(logger_config) +logger = logging.getLogger('database_logger') class UnitOfWork: def __init__(self, session: AsyncSession): @@ -21,7 +32,6 @@ async def rollback(self) -> None: async def close(self) -> None: await self.session.close() - @asynccontextmanager async def unit_of_work() -> AsyncIterator[UnitOfWork]: """Контекстный менеджер для работы с Unit of Work""" @@ -30,16 +40,29 @@ async def unit_of_work() -> AsyncIterator[UnitOfWork]: try: yield uow await uow.commit() - except DatabaseError as e: + except IntegrityError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка целостности данных', exc_info=e) + raise ValueError('UOW: Ошибка: нарушение целостности данных') from e + except OperationalError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка подключения к БД', exc_info=e) + raise ConnectionError('UOW: Ошибка подключения к базе данных') from e + except TimeoutError as e: await uow.rollback() - raise DatabaseError.from_sqlalchemy(e, 'unit of work') from e + logger.exception('Unit of work: Таймаут операции с БД', exc_info=e) + raise TimeoutError('UOW: Превышено время ожидания ответа от БД') from e + except SQLAlchemyError as e: + await uow.rollback() + logger.exception('Unit of work: Ошибка выполнения запроса', exc_info=e) + raise RuntimeError('UOW: Ошибка при выполнении запроса к БД') from e except Exception as e: + if isinstance(e, HTTPException) and getattr(e, 'status_code', 404) == 404: + # Пропускаем исключения и передаём вверх по стеку вызовов + logger.debug('Unit of work: пропускаем бизнес/клиентскую ошибку', exc_info=e) + raise await uow.rollback() - raise e + logger.critical('Unit of work: Непредвиденная ошибка', exc_info=e) + raise finally: await uow.close() - - -async def get_uow() -> AsyncIterator[UnitOfWork]: - async with unit_of_work() as uow: - yield uow \ No newline at end of file diff --git a/tasks/infrastructure/kafka/producer.py b/tasks/infrastructure/kafka/producer.py new file mode 100644 index 0000000..7978495 --- /dev/null +++ b/tasks/infrastructure/kafka/producer.py @@ -0,0 +1,38 @@ +import asyncio, json +from fastapi import FastAPI +from typing import AsyncGenerator +from contextlib import asynccontextmanager +from aiokafka import AIOKafkaProducer +from core.config import settings + + +async def _start_producer_with_retries(producer: AIOKafkaProducer): + last_exc = None + for i in range(1, settings.kafka.STARTUP_RETRIES + 1): + try: + await producer.start() + return + except Exception as exc: + last_exc = exc + await asyncio.sleep(settings.kafka.RETRY_BACKOFF * i) + raise last_exc + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + producer = AIOKafkaProducer( + bootstrap_servers=settings.kafka.BOOTSTRAP, + client_id=settings.kafka.CLIENT_ID, + value_serializer=lambda v: json.dumps(v).encode('utf-8'), + ) + # startup + await _start_producer_with_retries(producer) + app.state.kafka_producer = producer + app.state.kafka_producer_available = True + try: + yield + finally: + # shutdown + try: + await producer.stop() + except Exception: + pass \ No newline at end of file diff --git a/tasks/main.py b/tasks/main.py index 9c3106c..149d522 100644 --- a/tasks/main.py +++ b/tasks/main.py @@ -1,9 +1,12 @@ -import uvicorn - +import uvicorn, strawberry from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from core.config import settings -from api_v1.rest import router as router_v1 +from api_v1.rest import router as rest_api_router_v1 +from infrastructure.kafka.producer import lifespan +from strawberry.fastapi import GraphQLRouter +from api_v1.graphql.tasks.resolvers import Mutation, Query +from api_v1.graphql.context import get_context_wrapper import logging.config from core.logger import logger_config @@ -14,8 +17,10 @@ title='Tasks API', description='API for tasks', version='1.0.0', - # docs_url=None, - # redoc_url=None, + lifespan=lifespan, + docs_url=None, + redoc_url=None, + openapi_url=None ) app.add_middleware( @@ -26,7 +31,24 @@ allow_headers=settings.cors.headers, ) -app.include_router(router=router_v1, prefix=settings.api_v1_prefix) +app.include_router(router=rest_api_router_v1, prefix=settings.api_v1_prefix) + +schema = strawberry.Schema( + query=Query, + mutation=Mutation +) +graphql_app = GraphQLRouter( + schema=schema, + context_getter=get_context_wrapper, + graphql_ide='graphiql', + multipart_uploads_enabled=True +) + +app.include_router( + router=graphql_app, + prefix=settings.api_v1_prefix + '/graphql', + include_in_schema=False +) if __name__ == '__main__': diff --git a/tasks/migrations/env.py b/tasks/migrations/env.py index 25868fc..96d27d0 100644 --- a/tasks/migrations/env.py +++ b/tasks/migrations/env.py @@ -20,7 +20,7 @@ # add your model's MetaData object here # for 'autogenerate' support -from infrastructure.database.models import Base +from core.models import Base from core.config import settings target_metadata = Base.metadata diff --git a/tasks/poetry.lock b/tasks/poetry.lock index 1b3165b..5de7d7f 100644 --- a/tasks/poetry.lock +++ b/tasks/poetry.lock @@ -1,5 +1,58 @@ # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +[[package]] +name = "aiokafka" +version = "0.12.0" +description = "Kafka integration with asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiokafka-0.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da8938eac2153ca767ac0144283b3df7e74bb4c0abc0c9a722f3ae63cfbf3a42"}, + {file = "aiokafka-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5c827c8883cfe64bc49100de82862225714e1853432df69aba99f135969bb1b"}, + {file = "aiokafka-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea5710f7707ed12a7f8661ab38dfa80f5253a405de5ba228f457cc30404eb51"}, + {file = "aiokafka-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87b1a45c57bbb1c17d1900a74739eada27e4f4a0b0932ab3c5a8cbae8bbfe1e"}, + {file = "aiokafka-0.12.0-cp310-cp310-win32.whl", hash = "sha256:1158e630664d9abc74d8a7673bc70dc10737ff758e1457bebc1c05890f29ce2c"}, + {file = "aiokafka-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:06f5889acf8e1a81d6e14adf035acb29afd1f5836447fa8fa23d3cbe8f7e8608"}, + {file = "aiokafka-0.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddc5308c43d48af883667e2f950a0a9739ce2c9bfe69a0b55dc234f58b1b42d6"}, + {file = "aiokafka-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff63689cafcd6dd642a15de75b7ae121071d6162cccba16d091bcb28b3886307"}, + {file = "aiokafka-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24633931e05a9dc80555a2f845572b6845d2dcb1af12de27837b8602b1b8bc74"}, + {file = "aiokafka-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b2436c7c69384d210e9169fbfe339d9f49dbdcfddd8d51c79b9877de545e33"}, + {file = "aiokafka-0.12.0-cp311-cp311-win32.whl", hash = "sha256:90511a2c4cf5f343fc2190575041fbc70171654ab0dae64b3bbabd012613bfa7"}, + {file = "aiokafka-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:04c8ad27d04d6c53a1859687015a5f4e58b1eb221e8a7342d6c6b04430def53e"}, + {file = "aiokafka-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b01947553ff1120fa1cb1a05f2c3e5aa47a5378c720bafd09e6630ba18af02aa"}, + {file = "aiokafka-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3c8ec1c0606fa645462c7353dc3e4119cade20c4656efa2031682ffaad361c0"}, + {file = "aiokafka-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577c1c48b240e9eba57b3d2d806fb3d023a575334fc3953f063179170cc8964f"}, + {file = "aiokafka-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b815b2e5fed9912f1231be6196547a367b9eb3380b487ff5942f0c73a3fb5c"}, + {file = "aiokafka-0.12.0-cp312-cp312-win32.whl", hash = "sha256:5a907abcdf02430df0829ac80f25b8bb849630300fa01365c76e0ae49306f512"}, + {file = "aiokafka-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:fdbd69ec70eea4a8dfaa5c35ff4852e90e1277fcc426b9380f0b499b77f13b16"}, + {file = "aiokafka-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f9e8ab97b935ca681a5f28cf22cf2b5112be86728876b3ec07e4ed5fc6c21f2d"}, + {file = "aiokafka-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed991c120fe19fd9439f564201dd746c4839700ef270dd4c3ee6d4895f64fe83"}, + {file = "aiokafka-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c01abf9787b1c3f3af779ad8e76d5b74903f590593bc26f33ed48750503e7f7"}, + {file = "aiokafka-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08c84b3894d97fd02fcc8886f394000d0f5ce771fab5c498ea2b0dd2f6b46d5b"}, + {file = "aiokafka-0.12.0-cp313-cp313-win32.whl", hash = "sha256:63875fed922c8c7cf470d9b2a82e1b76b4a1baf2ae62e07486cf516fd09ff8f2"}, + {file = "aiokafka-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:bdc0a83eb386d2384325d6571f8ef65b4cfa205f8d1c16d7863e8d10cacd995a"}, + {file = "aiokafka-0.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9590554fae68ec80099beae5366f2494130535a1a3db0c4fa5ccb08f37f6e46"}, + {file = "aiokafka-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c77f5953ff4b25c889aef26df1f28df66c58db7abb7f34ecbe48502e9a6d273"}, + {file = "aiokafka-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96d7fd8fdb5f439f7e7860fd8ec37870265d0578475e82049bce60ab07ca045"}, + {file = "aiokafka-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ddff02b1e981083dff6d1a80d4502e0e83e0e480faf1f881766ca6f23e8d22"}, + {file = "aiokafka-0.12.0-cp39-cp39-win32.whl", hash = "sha256:4aab2767dcc8923626d8d60c314f9ba633563249cff71750db5d70b6ec813da2"}, + {file = "aiokafka-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:7a57fda053acd1b88c87803ad0381a1d2a29d36ec561550d11ce9154972b8e23"}, + {file = "aiokafka-0.12.0.tar.gz", hash = "sha256:62423895b866f95b5ed8d88335295a37cc5403af64cb7cb0e234f88adc2dff94"}, +] + +[package.dependencies] +async-timeout = "*" +packaging = "*" +typing-extensions = ">=4.10.0" + +[package.extras] +all = ["cramjam (>=2.8.0)", "gssapi"] +gssapi = ["gssapi"] +lz4 = ["cramjam (>=2.8.0)"] +snappy = ["cramjam"] +zstd = ["cramjam"] + [[package]] name = "alembic" version = "1.16.5" @@ -51,6 +104,18 @@ sniffio = ">=1.1" [package.extras] trio = ["trio (>=0.26.1)"] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -311,6 +376,18 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (> standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "graphql-core" +version = "3.2.6" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +optional = false +python-versions = "<4,>=3.6" +groups = ["main"] +files = [ + {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, + {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, +] + [[package]] name = "greenlet" version = "3.2.4" @@ -475,6 +552,99 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "lia-web" +version = "0.2.3" +description = "A library for working with web frameworks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "lia_web-0.2.3-py3-none-any.whl", hash = "sha256:237c779c943cd4341527fc0adfcc3d8068f992ee051f4ef059b8474ee087f641"}, + {file = "lia_web-0.2.3.tar.gz", hash = "sha256:ccc9d24cdc200806ea96a20b22fb68f4759e6becdb901bd36024df7921e848d7"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.0" + +[[package]] +name = "libcst" +version = "1.8.4" +description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "libcst-1.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:114343271f70a79e6d08bc395f5dfa150227341fab646cc0a58e80550e7659b7"}, + {file = "libcst-1.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b376ef7fa30bef611d4fb32af1da0e767b801b00322028a874ab3a441686b6a9"}, + {file = "libcst-1.8.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9be5b1b7d416900ff9bcdb4945692e6252fdcbd95514e98439f81568568c9e02"}, + {file = "libcst-1.8.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e4c5055e255d12745c7cc60fb5fb31c0f82855864c15dc9ad33a44f829b92600"}, + {file = "libcst-1.8.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b1e570ba816da408b5ee40ac479b34e56d995bf32dcca6f0ddb3d69b08e77de"}, + {file = "libcst-1.8.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65364c214251ed5720f3f6d0c4ef1338aac91ad4bbc5d30253eac21832b0943a"}, + {file = "libcst-1.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:a90c80e4d89222e11c7a734bc1b7f930bc2aba7750ad149bde1b136f839ea788"}, + {file = "libcst-1.8.4-cp310-cp310-win_arm64.whl", hash = "sha256:2d71e7e5982776f78cca9102286bb0895ef6f7083f76c0c9bc5ba4e9e40aee38"}, + {file = "libcst-1.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e7baaa6f01b6b6ea4b28d60204fddc679a3cd56d312beee200bd5f8f9711f0b"}, + {file = "libcst-1.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:259737faf90552a0589d95393dcaa3d3028be03ab3ea87478d46a1a4f922dd91"}, + {file = "libcst-1.8.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a65e3c409ef16ae369600d085d23a3897d4fccf4fdcc09294a402c513ac35906"}, + {file = "libcst-1.8.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fa870f34018c7241ee9227723cac0787599a2a8a2bfd53eacfbbe1ea1a272ae6"}, + {file = "libcst-1.8.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3eeba4edb40b2291c2460fe8d7e43f47e5fcc33f186675db5d364395adca3401"}, + {file = "libcst-1.8.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a5cd7beef667e5de3c5fb0ec387dc19aeda5cd4606ff541d0e8613bb3ef3b23"}, + {file = "libcst-1.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:3de575f0b5b466f2e9656b963f5848103cc518c6f3581902c6f430b07864584f"}, + {file = "libcst-1.8.4-cp311-cp311-win_arm64.whl", hash = "sha256:2fcff2130824f2cb5f4fd9c4c74fb639c5f02bc4228654461f6dc6b1006f20c0"}, + {file = "libcst-1.8.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1d468514a21cf3444dc3f3a4b1effc6c05255c98cc79e02af394652d260139f0"}, + {file = "libcst-1.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:870a49df8575c11ea4f5319d54750f95d2d06370a263bd42d924a9cf23cf0cbe"}, + {file = "libcst-1.8.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c9c775bc473225a0ad8422150fd9cf18ed2eebd7040996772937ac558f294d6c"}, + {file = "libcst-1.8.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27eeb16edb7dc0711d67e28bb8c0288e4147210aeb2434f08c16ac5db6b559e5"}, + {file = "libcst-1.8.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e12101ef2a6e05b7610badb2bfa597379289f1408e305a8d19faacdb872f47"}, + {file = "libcst-1.8.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:69b672c1afac5fe00d689f585ba57ac5facc4632f39b977d4b3e4711571c76e2"}, + {file = "libcst-1.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7832ee448fbdf18884a1f9af5fba1be6d5e98deb560514d92339fd6318aef651"}, + {file = "libcst-1.8.4-cp312-cp312-win_arm64.whl", hash = "sha256:6840e4011b583e9b7a71c00e7ab4281aea7456877b3ea6ecedb68a39a000bc64"}, + {file = "libcst-1.8.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e8d5158f976a5ee140ad0d3391e1a1b84b2ce5da62f16e48feab4bc21b91967"}, + {file = "libcst-1.8.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a179c712f38acb85e81d8949e80e05a422c92dcf5a00d8f4976f7e547a9f0916"}, + {file = "libcst-1.8.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d130f3e2d40c5f48cbbc804710ddf5b4db9dd7c0118f3b35f109164a555860d2"}, + {file = "libcst-1.8.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a4270123c988e130cec94bfe1b54d34784a40b34b2d5ac0507720c1272bd3209"}, + {file = "libcst-1.8.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea74c56cb11a1fdca9f8ab258965adce23e049ef525fdcc5c254a093e3de25cb"}, + {file = "libcst-1.8.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7fe97d432d95b6bcb1694a6d0fa7e07dde8fa687a637958126410ee2ced94b81"}, + {file = "libcst-1.8.4-cp313-cp313-win_amd64.whl", hash = "sha256:2c6d8f7087e9eaf005efde573f3f36d1d40366160155c195a6c4230d4c8a5839"}, + {file = "libcst-1.8.4-cp313-cp313-win_arm64.whl", hash = "sha256:062e424042c36a102abd11d8e9e27ac6be68e1a934b0ecfc9fb8fea017240d2f"}, + {file = "libcst-1.8.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:873dd4e8b896f7cb0e78118badda55ec1f42e9301a4a948cc438955ff3ae2257"}, + {file = "libcst-1.8.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:52c9376ba11ede5430e40aa205101dfc41202465103c6540f24591f898afb3d6"}, + {file = "libcst-1.8.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:074a3b17e270237fb36d3b94d7492fb137cb74217674484ba25e015e8d3d8bdc"}, + {file = "libcst-1.8.4-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:846aad04bac624a42d182add526d019e417e6a2b8a4c0bf690d32f9e1f3075ff"}, + {file = "libcst-1.8.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:93c76ab41d736b66d6fb3df32cd33184eed17666d7dc3ce047cf7ccdfe80b5b1"}, + {file = "libcst-1.8.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f167bf83dce662c9b499f1ea078ec2f2fee138e80f7d7dbd59c89ed28dc935f"}, + {file = "libcst-1.8.4-cp313-cp313t-win_amd64.whl", hash = "sha256:43cbb6b41bc2c4785136f59a66692287d527aeb022789c4af44ad6e85b7b2baa"}, + {file = "libcst-1.8.4-cp313-cp313t-win_arm64.whl", hash = "sha256:6cc8b7e33f6c4677e220dd7025e1e980da4d3f497b9b8ee0320e36dd54597f68"}, + {file = "libcst-1.8.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d011d731c2e673fbd9c84794418230a913ae3c98fc86f27814612b6b6d53d26b"}, + {file = "libcst-1.8.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a334dd11cdea34275df91c2ae9cc5933ec7e0ad5698264966708d637d110b627"}, + {file = "libcst-1.8.4-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:783f52b7c8d82046f0d93812f62a25eb82c3834f198e6cbfd5bb03ca68b593c8"}, + {file = "libcst-1.8.4-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0352c7d662c89243e730a28edf41577f87e28649c18ee365dd373c5fbdab2434"}, + {file = "libcst-1.8.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb188ebd4114144e14f6beb5499e43bebd0ca3ce7f2beb20921d49138c67b814"}, + {file = "libcst-1.8.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a718e5f6b398a07ca5d533e6593c1590d69fe65c539323281959733d6d541dd"}, + {file = "libcst-1.8.4-cp314-cp314-win_amd64.whl", hash = "sha256:fedfd33e5dda2200d582554e6476626d4706aa1fa2794bfb271879f8edff89b9"}, + {file = "libcst-1.8.4-cp314-cp314-win_arm64.whl", hash = "sha256:eff724c17df10e059915000eaf59f4e79998b66a7d35681e934a9a48667df931"}, + {file = "libcst-1.8.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:64cc34d74c9543b30ec3d7481dd644cb1bb3888076b486592d7fa0f22632f1c6"}, + {file = "libcst-1.8.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3ad7f0a32ddcdff00a3eddfd35cfd8485d9f357a32e4c67558476570199f808f"}, + {file = "libcst-1.8.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2e156760fc741bbf2fa68f4e3b15f019e924ea852f02276d0a53b7375cf70445"}, + {file = "libcst-1.8.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:fceb17616f1afe528c88243e3e7f78f84f0cc287463f04f3c1243e20a469e869"}, + {file = "libcst-1.8.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5db0b484670aac7ea442213afaa9addb1de0d9540a34ad44d376bec12242bc3a"}, + {file = "libcst-1.8.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:929798ca38ea76a5056f725221d66c6923e749caa9fa7f4cc86e914a3698493d"}, + {file = "libcst-1.8.4-cp314-cp314t-win_amd64.whl", hash = "sha256:e6f309c0f42e323c527d8c9007f583fd1668e45884208184a70644d916f27829"}, + {file = "libcst-1.8.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4b1cbadd988fee59b25ea154708cfed99cfaf45f9685707be422ad736371a9fe"}, + {file = "libcst-1.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fbadca1bc31f696875c955080c407a40b2d1aa7f79ca174a65dcb0542a57db6c"}, + {file = "libcst-1.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d4111f971632e9ddf8191aeef4576595e18ef3fa7b3016bfe15a08fa8554df"}, + {file = "libcst-1.8.4-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f5bd0bcdd2a8da9dad47d36d71757d8ba87baf887ae6982e2cb8621846610c49"}, + {file = "libcst-1.8.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2e24d11a1be0b1791f7bace9d406f5a70b8691ef77be377b606950803de4657d"}, + {file = "libcst-1.8.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:14bda1e4ea0b04d3926d41f6dafbfd311a951b75a60fe0d79bb5a8249c1cef5b"}, + {file = "libcst-1.8.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:056733760ba5ac1fd4cd518cddd5a43b3adbe2e0f6c7ce02532a114f7cd5d85b"}, + {file = "libcst-1.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:33664117fcb2913fdbd7de07a009193b660a16e7af18f7c1b4449e428f3b0f95"}, + {file = "libcst-1.8.4-cp39-cp39-win_arm64.whl", hash = "sha256:b69e94625702825309fd9e50760e77a5a60bd1e7a8e039862c8dd3011a6e1530"}, + {file = "libcst-1.8.4.tar.gz", hash = "sha256:f0f105d32c49baf712df2be360d496de67a2375bcf4e9707e643b7efc2f9a55a"}, +] + +[package.dependencies] +pyyaml-ft = {version = ">=8.0.0", markers = "python_version >= \"3.13\""} + [[package]] name = "mako" version = "1.3.10" @@ -495,6 +665,30 @@ babel = ["Babel"] lingua = ["lingua"] testing = ["pytest"] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + [[package]] name = "markupsafe" version = "3.0.2" @@ -566,6 +760,18 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -799,7 +1005,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -869,6 +1075,21 @@ pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] +[[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 = ["main"] +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.1.1" @@ -884,6 +1105,18 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -947,6 +1180,76 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +description = "YAML parser and emitter for Python with support for free-threading" +optional = false +python-versions = ">=3.13" +groups = ["main"] +files = [ + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, + {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, + {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, +] + +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[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 = ["main"] +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 = "sniffio" version = "1.3.1" @@ -1073,6 +1376,69 @@ anyio = ">=3.6.2,<5" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "strawberry-graphql" +version = "0.282.0" +description = "A library for creating GraphQL APIs" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "strawberry_graphql-0.282.0-py3-none-any.whl", hash = "sha256:6671cf90fd3630ae7d50aae2d8a06808ada6ce55d02ee01b7b7cba0026dfed18"}, + {file = "strawberry_graphql-0.282.0.tar.gz", hash = "sha256:f15610e4944210c15f83be814bfcd6ac1024b4293c18c9f4eee8345639795d1c"}, +] + +[package.dependencies] +graphql-core = ">=3.2.0,<3.4.0" +lia-web = ">=0.2.1" +libcst = {version = "*", optional = true, markers = "extra == \"debug-server\""} +packaging = ">=23" +pygments = {version = ">=2.3,<3.0", optional = true, markers = "extra == \"debug-server\""} +python-dateutil = ">=2.7,<3.0" +python-multipart = {version = ">=0.0.7", optional = true, markers = "extra == \"debug-server\""} +rich = {version = ">=12.0.0", optional = true, markers = "extra == \"debug-server\""} +starlette = {version = ">=0.18.0", optional = true, markers = "extra == \"debug-server\""} +typer = {version = ">=0.7.0", optional = true, markers = "extra == \"debug-server\""} +typing-extensions = ">=4.5.0" +uvicorn = {version = ">=0.11.6", optional = true, markers = "extra == \"debug-server\""} +websockets = {version = ">=15.0.1,<16", optional = true, markers = "extra == \"debug-server\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.7.4.post0,<4)"] +asgi = ["python-multipart (>=0.0.7)", "starlette (>=0.18.0)"] +chalice = ["chalice (>=1.22,<2.0)"] +channels = ["asgiref (>=3.2,<4.0)", "channels (>=3.0.5)"] +cli = ["libcst", "pygments (>=2.3,<3.0)", "rich (>=12.0.0)", "typer (>=0.7.0)"] +debug = ["libcst", "rich (>=12.0.0)"] +debug-server = ["libcst", "pygments (>=2.3,<3.0)", "python-multipart (>=0.0.7)", "rich (>=12.0.0)", "starlette (>=0.18.0)", "typer (>=0.7.0)", "uvicorn (>=0.11.6)", "websockets (>=15.0.1,<16)"] +django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] +fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] +flask = ["flask (>=1.1)"] +litestar = ["litestar (>=2) ; python_version ~= \"3.10\""] +opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] +pydantic = ["pydantic (>1.6.1)"] +pyinstrument = ["pyinstrument (>=4.0.0)"] +quart = ["quart (>=0.19.3)"] +sanic = ["sanic (>=20.12.2)"] + +[[package]] +name = "typer" +version = "0.19.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.19.1-py3-none-any.whl", hash = "sha256:914b2b39a1da4bafca5f30637ca26fa622a5bf9f515e5fdc772439f306d5682a"}, + {file = "typer-0.19.1.tar.gz", hash = "sha256:cb881433a4b15dacc875bb0583d1a61e78497806741f9aba792abcab390c03e6"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1379,5 +1745,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = ">=3.13" -content-hash = "9ac9a0b9e4f16343fead89749824dffe8cad8a1079a0cb80777004cbebeb3d2c" +python-versions = ">=3.13,<4.0" +content-hash = "7e55f8e0fe3f4adf29967a44546d309dcb1fdfb4b6ad7da9ed9bf773b3d17ffa" diff --git a/tasks/pyproject.toml b/tasks/pyproject.toml index 7733521..e69ff43 100644 --- a/tasks/pyproject.toml +++ b/tasks/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = [{ name = "Powermacintosh", email = "ak.powermacintosh@gmail.com" }] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.13,<4.0" dependencies = [ "fastapi (>=0.116.1,<0.117.0)", "uvicorn[standard] (>=0.35.0,<0.36.0)", @@ -14,6 +14,8 @@ dependencies = [ "asyncpg (>=0.30.0,<0.31.0)", "alembic (>=1.16.5,<2.0.0)", "black (>=25.1.0,<26.0.0)", + "aiokafka (>=0.12.0,<0.13.0)", + "strawberry-graphql[debug-server] (>=0.282.0,<0.283.0)", ] [tool.poetry] @@ -31,5 +33,5 @@ pytest-cov = "^7.0.0" [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" -addopts = "--cov=api_v1 --cov=infrastructure/database/repositories --cov-report=term-missing" +addopts = "--cov=api_v1 --cov=core/repositories --cov-report=term-missing" python_files = "test_*.py" diff --git a/tasks/tests/conftest.py b/tasks/tests/conftest.py index f8efd9d..1dc2b99 100644 --- a/tasks/tests/conftest.py +++ b/tasks/tests/conftest.py @@ -1,20 +1,17 @@ -import pytest -from main import app -from sqlalchemy.ext.asyncio import AsyncSession +import pytest, asyncio, uuid from typing import AsyncIterator +from aiokafka import AIOKafkaConsumer from httpx import AsyncClient -from sqlalchemy.ext.asyncio import ( - create_async_engine, - async_sessionmaker, -) +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker +from main import app from core.config import settings @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config, items): - if settings.db.MODE != 'TEST': + if settings.MODE != 'TEST': for item in items: - item.add_marker(pytest.mark.skip(reason=f'В режиме {settings.db.MODE}, тесты недоступны!')) + item.add_marker(pytest.mark.skip(reason=f'В режиме {settings.MODE}, тесты недоступны!')) class TestingUnitOfWork: def __init__(self, session: AsyncSession): @@ -59,3 +56,22 @@ async def httpx_client(): async with AsyncClient(base_url=f'http://tasks_app_test:{settings.api_v1_port}') as async_client: yield async_client +@pytest.fixture(scope='function') +async def kafka_consumer() -> AIOKafkaConsumer: + """Фикстура Kafka Consumer.""" + consumer = AIOKafkaConsumer( + settings.kafka.TOPIC, + bootstrap_servers=settings.kafka.BOOTSTRAP, + group_id=f'test-group-{uuid.uuid4()}', + enable_auto_commit=False, + auto_offset_reset='earliest', + ) + await consumer.start() + try: + await asyncio.sleep(1) + await consumer.seek_to_beginning() + yield consumer + finally: + await consumer.stop() + + diff --git a/tasks/tests/integration/test_graphql_api.py b/tasks/tests/integration/test_graphql_api.py new file mode 100644 index 0000000..af9757c --- /dev/null +++ b/tasks/tests/integration/test_graphql_api.py @@ -0,0 +1,236 @@ +import pytest, asyncio, httpx +from typing import Dict, Any, Optional +from httpx import AsyncClient +from core.config import settings + + +class TestTaskGraphQLAPI: + """ + Тесты для GraphQL API менеджера задач. + """ + + GRAPHQL_ENDPOINT = '/api/v1/graphql' + + async def wait_for_server(self, url: str, timeout: int = 30) -> bool: + """Ожидает подключения к серверу.""" + base_url=f'http://tasks_app_test:{settings.api_v1_port}' + start_time = asyncio.get_event_loop().time() + while True: + try: + async with httpx.AsyncClient() as client: + await client.get(base_url + url) + return True + except httpx.ConnectError: + if asyncio.get_event_loop().time() - start_time > timeout: + raise TimeoutError('Не удалось подключиться к серверу за отведенное время.') + await asyncio.sleep(1) + + async def execute_graphql_query( + self, + httpx_client: AsyncClient, + query: str, + variables: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: + """Выполняет GraphQL запрос и возвращает ответ.""" + response = await httpx_client.post( + self.GRAPHQL_ENDPOINT, + json={ + 'query': query, + 'variables': variables or {} + }, + headers=headers or {} + ) + return response.json() + + @pytest.fixture + async def test_create_task(self, httpx_client: AsyncClient) -> Dict[str, Any]: + """Фикстура для создания тестовой задачи через GraphQL.""" + await self.wait_for_server(self.GRAPHQL_ENDPOINT) + + create_mutation = """ + mutation CreateTask($input: TaskCreateInputGQL!) { + createTask(input: $input) { + id + title + description + status + } + } + """ + variables = { + 'input': { + 'title': 'Test Task', + 'description': 'Test Description' + } + } + response = await self.execute_graphql_query( + httpx_client, + create_mutation, + variables=variables + ) + + assert 'data' in response, f'Ошибка при создании задачи: {response}' + assert 'createTask' in response['data'], f'Ошибка в ответе: {response}' + + task = response['data']['createTask'] + assert task['title'] == 'Test Task' + assert task['description'] == 'Test Description' + assert task['status'] == 'CREATED' + + return task + + @pytest.mark.asyncio + async def test_get_task(self, httpx_client: AsyncClient, test_create_task: Dict[str, Any]): + """Тест получения задачи по ID.""" + task_id = test_create_task['id'] + + query = """ + query GetTask($id: ID!) { + task(id: $id) { + id + title + description + status + } + } + """ + + response = await self.execute_graphql_query( + httpx_client, + query, + variables={'id': task_id} + ) + + assert 'data' in response, f'Ошибка при получении задачи: {response}' + assert 'task' in response['data'], f'Ошибка в ответе: {response}' + + task = response['data']['task'] + assert task['id'] == task_id + assert task['title'] == 'Test Task' + assert task['description'] == 'Test Description' + assert task['status'] == 'CREATED' + + @pytest.mark.asyncio + async def test_get_tasks_list(self, httpx_client: AsyncClient, test_create_task: Dict[str, Any]): + """Тест получения списка задач.""" + query = """ + query GetTasks($status: TaskStatusGQL, $offset: Int, $limit: Int) { + tasks(status: $status, offset: $offset, limit: $limit) { + tasks { + id + title + status + } + total + pagesCount + } + } + """ + + # Получаем все задачи + response = await self.execute_graphql_query( + httpx_client, + query, + variables={'status': 'CREATED', 'offset': 0, 'limit': 10} + ) + + assert 'data' in response, f'Ошибка при получении списка задач: {response}' + assert 'tasks' in response['data'], f'Ошибка в ответе: {response}' + + tasks_page = response['data']['tasks'] + assert isinstance(tasks_page['tasks'], list) + assert tasks_page['total'] > 0 + assert tasks_page['pagesCount'] > 0 + + # Проверяем, что созданная задача есть в списке + task_ids = [task['id'] for task in tasks_page['tasks']] + assert test_create_task['id'] in task_ids + + @pytest.mark.asyncio + async def test_update_task(self, httpx_client: AsyncClient, test_create_task: Dict[str, Any]): + """Тест обновления задачи.""" + task_id = test_create_task['id'] + + update_mutation = """ + mutation UpdateTask($id: ID!, $input: TaskUpdateInputGQL!) { + updateTask(id: $id, input: $input) { + id + title + description + status + } + } + """ + + variables = { + 'id': task_id, + 'input': { + 'title': 'Updated Task', + 'description': 'Updated Description', + 'status': 'IN_PROGRESS' + } + } + + response = await self.execute_graphql_query( + httpx_client, + update_mutation, + variables=variables + ) + + assert 'data' in response, f'Ошибка при обновлении задачи: {response}' + assert 'updateTask' in response['data'], f'Ошибка в ответе: {response}' + + updated_task = response['data']['updateTask'] + assert updated_task['id'] == task_id + assert updated_task['title'] == 'Updated Task' + assert updated_task['description'] == 'Updated Description' + assert updated_task['status'] == 'IN_PROGRESS' + + @pytest.mark.asyncio + async def test_delete_task(self, httpx_client: AsyncClient, test_create_task: Dict[str, Any]): + """Тест удаления задачи.""" + task_id = test_create_task['id'] + + delete_mutation = """ + mutation DeleteTask($id: ID!) { + deleteTask(id: $id) + } + """ + + # Удаляем задачу + response = await self.execute_graphql_query( + httpx_client, + delete_mutation, + variables={'id': task_id} + ) + + assert 'data' in response, f'Ошибка при удалении задачи: {response}' + assert 'deleteTask' in response['data'], f'Ошибка в ответе: {response}' + assert response['data']['deleteTask'] is True + + # Проверяем, что задача действительно удалена + query = """ + query GetTask($id: ID!) { + task(id: $id) { + id + } + } + """ + + response = await self.execute_graphql_query( + httpx_client, + delete_mutation, + variables={'id': task_id} + ) + assert response['data']['deleteTask'] is False + + response = await self.execute_graphql_query( + httpx_client, + query, + variables={'id': task_id} + ) + + # Задача не должна быть найдена + assert 'data' in response, f'Ошибка при проверке удаления задачи: {response}' + assert response['data']['task'] is None diff --git a/tasks/tests/integration/test_kafka.py b/tasks/tests/integration/test_kafka.py new file mode 100644 index 0000000..ea2ccb0 --- /dev/null +++ b/tasks/tests/integration/test_kafka.py @@ -0,0 +1,62 @@ +import pytest, asyncio, httpx, json +from fastapi import status +from httpx import AsyncClient +from aiokafka import AIOKafkaConsumer +from core.config import settings + + +class TestTaskKafkaIntegration: + """ + Тесты для интеграции менеджера задач с использованием Kafka. + """ + async def wait_for_server(self, url: str, timeout: int = 30) -> bool: + """ + Ожидает подключения к серверу. + """ + base_url=f'http://tasks_app_test:{settings.api_v1_port}' + start_time = asyncio.get_event_loop().time() + while True: + try: + async with httpx.AsyncClient() as client: + await client.get(base_url + url) + return True + except httpx.ConnectError: + if asyncio.get_event_loop().time() - start_time > timeout: + raise TimeoutError('Не удалось подключиться к серверу за отведенное время.') + await asyncio.sleep(1) + + @pytest.mark.asyncio + async def test_create_task_event( + self, httpx_client: AsyncClient, + kafka_consumer: AIOKafkaConsumer + ): + """ + Тест корректного создания задачи через Kafka. + """ + url = '/api/v1/task/create_event' + await self.wait_for_server(url) + response_task = await httpx_client.post( + url, + json={ + 'title': 'Create Task', + 'description': None, + } + ) + data_response_task = response_task.json() + assert response_task.status_code == status.HTTP_201_CREATED + assert data_response_task['status'] == 'published' + + # Пробуем поймать + received = None + for _ in range(25): + try: + msg = await asyncio.wait_for(kafka_consumer.getone(), timeout=2) + if msg: + received = json.loads(msg.value.decode('utf-8')) + break + except Exception: + await asyncio.sleep(1) + + assert received is not None + assert received['event'] == 'TaskModuleCreation' + assert received['task']['title'] == 'Create Task' diff --git a/tasks/tests/performance/test_performance_rest_api.py b/tasks/tests/performance/test_performance_rest_api.py index bbce04a..7f033a0 100644 --- a/tasks/tests/performance/test_performance_rest_api.py +++ b/tasks/tests/performance/test_performance_rest_api.py @@ -7,9 +7,10 @@ class TestPerformanceTaskRestAPI: """ Тесты производительности для REST API менеджера задач. """ - @pytest.mark.skipif(sys.platform == 'linux', reason='Тест работает только на Linux') - def test_only_on_linux(self): - # Тест, который работает только на Linux. + @pytest.mark.skipif(sys.platform not in ('darwin', 'linux'), reason='Тест работает только на MacOS или Linux') + def test_only_on_macos_or_linux(self): + # Тест, который работает только на MacOS или Linux. + print(f'Текущая ОС: {sys.platform}') assert True def setup_method(self): diff --git a/tasks/tests/unit/test_database_exceptions.py b/tasks/tests/unit/test_database_exceptions.py deleted file mode 100644 index afefa1a..0000000 --- a/tasks/tests/unit/test_database_exceptions.py +++ /dev/null @@ -1,213 +0,0 @@ -import pytest -from unittest.mock import patch, MagicMock -from sqlalchemy.exc import ( - IntegrityError, - OperationalError, - TimeoutError as SQLTimeoutError, - SQLAlchemyError -) - -from infrastructure.database.exceptions import ( - DatabaseError, - DatabaseIntegrityError, - DatabaseConnectionError, - DatabaseQueryError, - DatabaseTimeoutError, - DatabaseUnknownError -) - - -def test_database_error_initialization(): - """Тест базового DatabaseError""" - # Тест с кастомным сообщением - error = DatabaseError('Test error') - assert str(error) == 'Test error' - assert error.message == 'Test error' - assert error.original_exception is None - - # Тест с пустым сообщением - empty_error = DatabaseError('') - assert str(empty_error) == '' - assert empty_error.message == '' - - # Тест с числовым сообщением - numeric_error = DatabaseError(123) - assert str(numeric_error) == '123' - assert str(numeric_error.message) == '123' # Convert to string for comparison - - -def test_database_error_with_original_exception(): - """Тест DatabaseError с исключением""" - # Тест с простым исключением - original = ValueError('Original error') - error = DatabaseError('Test error', original) - assert error.original_exception is original - assert str(error) == 'Test error' - - # Тест с SQLAlchemy исключением - sqlalchemy_error = IntegrityError('Integrity error', None, None) - db_error = DatabaseError('DB error', sqlalchemy_error) - assert db_error.original_exception is sqlalchemy_error - assert str(db_error) == 'DB error' - - -def test_database_integrity_error(): - """Тест DatabaseIntegrityError""" - # Тест с кастомным сообщением - original = IntegrityError('Integrity error', None, None) - error = DatabaseIntegrityError('Test integrity error', original) - assert isinstance(error, DatabaseError) - assert 'Test integrity error' in str(error) - - # Тест с сообщением по умолчанию - error_default = DatabaseIntegrityError(original_exception=original) - assert 'Ошибка целостности базы данных' in str(error_default) - - -def test_database_connection_error(): - """Тест DatabaseConnectionError""" - # Тест с кастомным сообщением - original = OperationalError('Connection failed', None, None) - error = DatabaseConnectionError('Test connection error', original) - assert isinstance(error, DatabaseError) - assert 'Test connection error' in str(error) - - # Тест с сообщением по умолчанию - error_default = DatabaseConnectionError(original_exception=original) - assert 'Ошибка подключения к базе данных' in str(error_default) - - -def test_database_timeout_error(): - """Тест DatabaseTimeoutError""" - # Тест с кастомным сообщением - original = SQLTimeoutError('Query timed out', None, None) - error = DatabaseTimeoutError('Test timeout error', original) - assert isinstance(error, DatabaseError) - assert 'Test timeout error' in str(error) - - # Тест с сообщением по умолчанию - error_default = DatabaseTimeoutError(original_exception=original) - assert 'Ошибка таймаута базы данных' in str(error_default) - - -def test_database_unknown_error(): - """Тест DatabaseUnknownError""" - # Тест с кастомным сообщением - original = Exception('Unknown error') - error = DatabaseUnknownError('Test unknown error', original) - assert isinstance(error, DatabaseError) - assert 'Test unknown error' in str(error) - - # Тест с сообщением по умолчанию - error_default = DatabaseUnknownError(original_exception=original) - assert 'Непредвиденная ошибка' in str(error_default) - - -def test_from_sqlalchemy_integrity_error(): - """Тест from_sqlalchemy с IntegrityError""" - # Создаем мок для IntegrityError - error_msg = "(psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint" - original = IntegrityError(error_msg, None, None) - - # Тест с контекстом - error = DatabaseError.from_sqlalchemy(original, 'создание пользователя') - assert isinstance(error, DatabaseIntegrityError) - assert 'создание пользователя' in str(error) - assert 'Ошибка базы данных в создание пользователя' in str(error) - assert error_msg in str(error) - - # Тест без контекста - error_no_context = DatabaseError.from_sqlalchemy(original) - assert isinstance(error_no_context, DatabaseIntegrityError) - assert 'Ошибка базы данных' in str(error_no_context) - assert error_msg in str(error_no_context) - - -def test_from_sqlalchemy_operational_error(): - """Тест from_sqlalchemy с OperationalError""" - error_msg = "(psycopg2.OperationalError) could not connect to server" - original = OperationalError(error_msg, None, None) - - # Тест с контекстом - error = DatabaseError.from_sqlalchemy(original, 'подключение к БД') - assert isinstance(error, DatabaseConnectionError) - assert 'подключение к БД' in str(error) - assert 'Ошибка базы данных в подключение к БД' in str(error) - assert error_msg in str(error) - - # Тест без контекста - error_no_context = DatabaseError.from_sqlalchemy(original) - assert isinstance(error_no_context, DatabaseConnectionError) - assert 'Ошибка базы данных' in str(error_no_context) - assert error_msg in str(error_no_context) - - -def test_from_sqlalchemy_timeout_error(): - """Тест from_sqlalchemy с TimeoutError""" - error_msg = "TimeoutError: Query timed out after 30s" - original = SQLTimeoutError(error_msg, None, None, None) - - # Тест с контекстом - error = DatabaseError.from_sqlalchemy(original, 'выполнение запроса') - assert isinstance(error, DatabaseTimeoutError) - assert 'выполнение запроса' in str(error) - assert 'Ошибка базы данных в выполнение запроса' in str(error) - assert error_msg in str(error) - - # Тест без контекста - error_no_context = DatabaseError.from_sqlalchemy(original) - assert isinstance(error_no_context, DatabaseTimeoutError) - assert 'Ошибка базы данных' in str(error_no_context) - assert error_msg in str(error_no_context) - - -def test_from_sqlalchemy_generic_sqlalchemy_error(): - """Тест from_sqlalchemy с общим SQLAlchemyError""" - error_msg = "(sqlalchemy.exc.SQLAlchemyError) An error occurred" - original = SQLAlchemyError(error_msg) - - # Тест с контекстом - error = DatabaseError.from_sqlalchemy(original, 'выполнении запроса') - assert isinstance(error, DatabaseQueryError) - assert 'выполнении запроса' in str(error) - assert 'Ошибка базы данных в выполнении запроса' in str(error) - assert error_msg in str(error) - - # Тест без контекста - error_no_context = DatabaseError.from_sqlalchemy(original) - assert isinstance(error_no_context, DatabaseQueryError) - assert 'Ошибка базы данных' in str(error_no_context) - assert error_msg in str(error_no_context) - - -def test_from_sqlalchemy_unknown_error(): - """Тест from_sqlalchemy с неизвестным исключением""" - error_msg = "Неожиданная ошибка при работе с БД" - original = Exception(error_msg) - - # Тест с контекстом - error = DatabaseError.from_sqlalchemy(original, 'неизвестной операции') - assert isinstance(error, DatabaseUnknownError) - assert 'неизвестной операции' in str(error) - assert 'Ошибка базы данных в неизвестной операции' in str(error) - assert error_msg in str(error) - assert error.original_exception is original - - # Тест без контекста - error_no_context = DatabaseError.from_sqlalchemy(original) - assert isinstance(error_no_context, DatabaseUnknownError) - assert 'Ошибка базы данных' in str(error_no_context) - assert error_msg in str(error_no_context) - assert error_no_context.original_exception is original - - -@patch('infrastructure.database.exceptions.logger.exception') -def test_error_logging(mock_logger): - """Тестирование логирования ошибок""" - original = ValueError('Test error') - error = DatabaseError('Test error', original) - mock_logger.assert_called_once() - - -# if __name__ == "__main__": -# pytest.main(["-v", "test_database_exceptions.py"]) diff --git a/tasks/tests/unit/test_task_repository.py b/tasks/tests/unit/test_task_repository.py index f56f4f8..bc3ef9d 100644 --- a/tasks/tests/unit/test_task_repository.py +++ b/tasks/tests/unit/test_task_repository.py @@ -1,10 +1,9 @@ import pytest from sqlalchemy import select from typing import AsyncIterator - -from infrastructure.database.models.task import Task, TaskStatus -from infrastructure.database.repositories.task import TaskRepository -from api_v1.rest.tasks.schemas import TaskCreate, TaskUpdate, TaskUpdatePartial +from core.models.task import Task, TaskStatus +from core.repositories.task import TaskRepository +from core.schemas.tasks import TaskCreate, TaskUpdate, TaskUpdatePartial class TestTaskRepository: """Тесты для TaskRepository""" diff --git a/tasks/tests/unit/test_task_validation.py b/tasks/tests/unit/test_task_validation.py new file mode 100644 index 0000000..3a14aad --- /dev/null +++ b/tasks/tests/unit/test_task_validation.py @@ -0,0 +1,41 @@ +import pytest +from sqlalchemy import exc +from typing import AsyncIterator +from pydantic import ValidationError +from core.repositories.task import TaskRepository +from core.schemas.tasks import TaskCreate + +class TestTaskPydanticValidation: + """Тесты валидации данных задачи.""" + + @pytest.mark.asyncio + async def test_pydantic_create_failed_max_length_title_task(self, testing_db_connection: AsyncIterator): + repo = TaskRepository(testing_db_connection.session) + + # Теперь Pydantic выбросит ValidationError при создании модели + with pytest.raises(ValidationError) as exc_info: + task = TaskCreate(title='Test task' * 100) + await repo.create_task(task) + + # Проверяем, что ошибка связана с превышением максимальной длины + assert 'String should have at most 100 characters' in str(exc_info.value) + + +class TestTaskDatabaseValidation: + """Тесты валидации данных задачи.""" + + @pytest.mark.asyncio + async def test_dbapi_create_failed_max_length_title_task(self, testing_db_connection: AsyncIterator): + repo = TaskRepository(testing_db_connection.session) + + task = TaskCreate(title='Test task') + task.title = 'Test task' * 100 + # Ожидаем, что при сохранении выбросится DBAPIError + with pytest.raises(exc.DBAPIError) as exc_info: + await repo.create_task(task) + + # Проверяем, что в сообщении об ошибке есть информация о превышении длины + assert 'value too long for type character varying(100)' in str(exc_info.value) + + # Явно откатываем транзакцию после ошибки + await testing_db_connection.rollback() \ No newline at end of file diff --git a/tasks/worker.py b/tasks/worker.py new file mode 100644 index 0000000..1b568a4 --- /dev/null +++ b/tasks/worker.py @@ -0,0 +1,57 @@ +import asyncio, json +from aiokafka import AIOKafkaConsumer, TopicPartition, ConsumerRecord +from api_v1.rest.tasks.dependencies import unit_of_work +from api_v1.service.task import TaskService +from core.schemas.tasks import TaskCreate +from core.config import settings + +import logging.config +from core.logger import logger_config + +logging.config.dictConfig(logger_config) +logger = logging.getLogger('kafka_logger') + + +async def handle_task_creation_message(msg: ConsumerRecord, consumer: AIOKafkaConsumer): + try: + data = json.loads(msg.value.decode()) + except Exception as e: + logger.exception('Невалидный json', exc_info=e) + await consumer.commit({TopicPartition(msg.topic, msg.partition): msg.offset + 1}) + raise + + try: + task = TaskCreate.model_validate(data['task']) + except Exception as e: + logger.exception('Невалидный payload', exc_info=e) + await consumer.commit({TopicPartition(msg.topic, msg.partition): msg.offset + 1}) + raise + + try: + async with unit_of_work() as uow: + svc = TaskService(uow) + await svc.create_task(task) + # Подтверждаем offset только если все успешно + logger.info('Сообщение успешно обработано => топик: %s, партиция: %s, offset: %s, ключ: %s, значение: %s, время: %s', msg.topic, msg.partition, msg.offset, msg.key, msg.value, msg.timestamp) + await consumer.commit({TopicPartition(msg.topic, msg.partition): msg.offset + 1}) + except Exception as e: + logger.exception('Ошибка записи в базу данных', exc_info=e) + raise + +async def run_consumer(): + consumer = AIOKafkaConsumer( + settings.kafka.TOPIC, + bootstrap_servers=settings.kafka.BOOTSTRAP, + group_id=settings.kafka.GROUP_ID, + enable_auto_commit=False, + auto_offset_reset='earliest' # начать с самого старого сообщения + ) + await consumer.start() + try: + async for msg in consumer: + await handle_task_creation_message(msg, consumer) + finally: + await consumer.stop() + +if __name__ == '__main__': + asyncio.run(run_consumer())