Compare commits
No commits in common. "d0e589798488ee7ee71b2ca621d80b823de84f2d" and "419505a0aab7efb1ee7fc74882d37e8cdbf74f08" have entirely different histories.
d0e5897984
...
419505a0aa
|
|
@ -1,60 +0,0 @@
|
|||
# Git ignore file
|
||||
# Байткод и кэш
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# Виртуальные окружения
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Файлы IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Дистрибутивы и сборки
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
.eggs/
|
||||
|
||||
# Pytest
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.cover
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Переменные окружения и секреты
|
||||
.env
|
||||
.env.local
|
||||
*.pem
|
||||
|
||||
# Логи и базы данных
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# Системные файлы
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# MyPy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
|
||||
# Документация
|
||||
docs/_build/
|
||||
306
TZ.md
306
TZ.md
|
|
@ -1,306 +0,0 @@
|
|||
# ТЗ: `dataloader` (один пакет, async, PG-очередь, LISTEN/NOTIFY)
|
||||
|
||||
## 1) Назначение и рамки
|
||||
|
||||
`dataloader` — сервис постановки и исполнения долгих ETL-задач через одну общую очередь в Postgres. Сервис предоставляет HTTP-ручки для триггера задач, мониторинга статуса и отмены; внутри процесса запускает N асинхронных воркеров, которые конкурируют за задачи через `SELECT … FOR UPDATE SKIP LOCKED`, держат lease/heartbeat, делают идемпотентные записи в целевые БД и корректно обрабатывают повторы.
|
||||
|
||||
Архитектura и инфраструктурные части соответствуют шаблону `rest_template.md`: единый пакет, `os_router.py` с `/health` и `/status`, middleware логирования, структура каталогов и конфиг-классы — **как в шаблоне**.
|
||||
|
||||
---
|
||||
|
||||
## 2) Архитектура (одно приложение, async)
|
||||
|
||||
* **FastAPI-приложение**: HTTP API v1, инфраструктурные роуты (`/health`, `/status`) из шаблона, middleware и логирование из шаблона.
|
||||
* **WorkerManager**: на `startup` читает конфиг (`WORKERS_JSON`) и поднимает M асинхронных воркер-циклов (по очередям и уровням параллелизма). На `shutdown` — мягкая остановка.
|
||||
* **PG Queue**: одна таблица `dl_jobs` на все очереди и сервисы; журнал `dl_job_events`; триггеры LISTEN/NOTIFY для пробуждения воркеров без активного поллинга.
|
||||
|
||||
---
|
||||
|
||||
## 3) Структура репозитория (один пакет, как в шаблоне)
|
||||
|
||||
```
|
||||
dataloader/
|
||||
├── src/
|
||||
│ └── dataloader/
|
||||
│ ├── __main__.py # точка входа FastAPI + запуск WorkerManager (по шаблону)
|
||||
│ ├── config.py # Pydantic Settings: DSN, тайминги, WORKERS_JSON
|
||||
│ ├── base.py
|
||||
│ ├── context.py # AppContext: engine/sessionmaker, DI
|
||||
│ ├── exceptions.py
|
||||
│ ├── logger/ # не менять тип и контракты
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── context_vars.py
|
||||
│ │ ├── logger.py
|
||||
│ │ ├── models.py
|
||||
│ │ ├── utils.py
|
||||
│ │ └── uvicorn_logging_config.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py # регистрация роутов (v1, os_router, metric_router)
|
||||
│ │ ├── middleware.py
|
||||
│ │ ├── os_router.py # /health, /status
|
||||
│ │ ├── metric_router.py
|
||||
│ │ └── v1/
|
||||
│ │ ├── router.py # POST /jobs/trigger, GET /jobs/{id}/status, POST /jobs/{id}/cancel
|
||||
│ │ ├── schemas.py # pydantic запросы/ответы
|
||||
│ │ ├── service.py # бизнес-логика
|
||||
│ │ ├── models.py
|
||||
│ │ ├── exceptions.py
|
||||
│ │ └── utils.py
|
||||
│ ├── storage/
|
||||
│ │ ├── db.py # async engine + sessionmaker
|
||||
│ │ └── repositories.py # SQL-операции по очереди и событиям
|
||||
│ └── workers/
|
||||
│ ├── manager.py # создание asyncio Tasks воркеров по конфигу
|
||||
│ ├── base.py # общий PG-воркер: claim/lease/heartbeat/retry
|
||||
│ └── pipelines/
|
||||
│ ├── __init__.py
|
||||
│ └── registry.py # реестр обработчиков по task
|
||||
├── tests/
|
||||
│ └── integration_tests/
|
||||
│ ├── conftest.py
|
||||
│ └── v1_api/
|
||||
│ ├── constants.py
|
||||
│ └── test_service.py
|
||||
├── pyproject.toml
|
||||
├── Dockerfile
|
||||
├── .env
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
Структура, ролевые файлы и подход соответствуют `rest_template.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4) DDL очереди (общая для всех сервисов)
|
||||
|
||||
> Таблицы уже созданы и доступны приложению.
|
||||
|
||||
```sql
|
||||
CREATE TYPE dl_status AS ENUM ('queued','running','succeeded','failed','canceled','lost');
|
||||
|
||||
CREATE TABLE dl_jobs (
|
||||
job_id uuid PRIMARY KEY,
|
||||
queue text NOT NULL,
|
||||
task text NOT NULL,
|
||||
args jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
idempotency_key text UNIQUE,
|
||||
lock_key text NOT NULL,
|
||||
partition_key text NOT NULL DEFAULT '',
|
||||
priority int NOT NULL DEFAULT 100,
|
||||
available_at timestamptz NOT NULL DEFAULT now(),
|
||||
status dl_status NOT NULL DEFAULT 'queued',
|
||||
attempt int NOT NULL DEFAULT 0,
|
||||
max_attempts int NOT NULL DEFAULT 5,
|
||||
lease_ttl_sec int NOT NULL DEFAULT 60,
|
||||
lease_expires_at timestamptz,
|
||||
heartbeat_at timestamptz,
|
||||
cancel_requested boolean NOT NULL DEFAULT false,
|
||||
progress jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
error text,
|
||||
producer text,
|
||||
consumer_group text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
CONSTRAINT dl_jobs_chk_positive CHECK (priority >= 0 AND attempt >= 0 AND max_attempts >= 0 AND lease_ttl_sec > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_dl_jobs_claim ON dl_jobs(queue, available_at, priority, created_at)
|
||||
WHERE status = 'queued';
|
||||
|
||||
CREATE INDEX ix_dl_jobs_running_lease ON dl_jobs(lease_expires_at)
|
||||
WHERE status = 'running';
|
||||
|
||||
CREATE INDEX ix_dl_jobs_status_queue ON dl_jobs(status, queue);
|
||||
|
||||
CREATE TABLE dl_job_events (
|
||||
event_id bigserial PRIMARY KEY,
|
||||
job_id uuid NOT NULL REFERENCES dl_jobs(job_id) ON DELETE CASCADE,
|
||||
queue text NOT NULL,
|
||||
ts timestamptz NOT NULL DEFAULT now(),
|
||||
kind text NOT NULL,
|
||||
payload jsonb
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_job_ready() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (TG_OP = 'INSERT') THEN
|
||||
PERFORM pg_notify('dl_jobs', NEW.queue);
|
||||
RETURN NEW;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
IF NEW.status = 'queued' AND NEW.available_at <= now()
|
||||
AND (OLD.status IS DISTINCT FROM NEW.status OR OLD.available_at IS DISTINCT FROM NEW.available_at) THEN
|
||||
PERFORM pg_notify('dl_jobs', NEW.queue);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS dl_jobs_notify_ins ON dl_jobs;
|
||||
CREATE TRIGGER dl_jobs_notify_ins
|
||||
AFTER INSERT ON dl_jobs
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_job_ready();
|
||||
|
||||
DROP TRIGGER IF EXISTS dl_jobs_notify_upd ON dl_jobs;
|
||||
CREATE TRIGGER dl_jobs_notify_upd
|
||||
AFTER UPDATE OF status, available_at ON dl_jobs
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_job_ready();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Контракты API (v1)
|
||||
|
||||
* `POST /api/v1/jobs/trigger`
|
||||
Вход: `{queue: str, task: str, args?: dict, idempotency_key?: str, lock_key: str, partition_key?: str, priority?: int, available_at?: RFC3339}`
|
||||
Выход: `{job_id: UUID, status: str}`
|
||||
Поведение: идемпотентная постановка; триггер LISTEN/NOTIFY срабатывает за счёт триггера в БД.
|
||||
|
||||
* `GET /api/v1/jobs/{job_id}/status`
|
||||
Выход: `{job_id, status, attempt, started_at?, finished_at?, heartbeat_at?, error?, progress: {}}`
|
||||
|
||||
* `POST /api/v1/jobs/{job_id}/cancel`
|
||||
Выход: `{… как status …}`
|
||||
Поведение: устанавливает `cancel_requested = true`. Воркер кооперативно завершает задачу между чанками.
|
||||
|
||||
Инфраструктурные эндпоинты `/health`, `/status`, мидлвар и регистрация роутов — **как в шаблоне**.
|
||||
|
||||
---
|
||||
|
||||
## 6) Протокол выполнения (воркер)
|
||||
|
||||
1. **Claim** одной задачи:
|
||||
|
||||
```sql
|
||||
WITH cte AS (
|
||||
SELECT job_id
|
||||
FROM dl_jobs
|
||||
WHERE status='queued' AND queue=:queue AND available_at <= now()
|
||||
ORDER BY priority ASC, created_at ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
UPDATE dl_jobs j
|
||||
SET status='running',
|
||||
started_at = COALESCE(started_at, now()),
|
||||
attempt = attempt + 1,
|
||||
lease_expires_at = now() + make_interval(secs => j.lease_ttl_sec),
|
||||
heartbeat_at = now()
|
||||
FROM cte
|
||||
WHERE j.job_id = cte.job_id
|
||||
RETURNING j.job_id, j.task, j.args, j.lock_key, j.partition_key, j.lease_ttl_sec;
|
||||
```
|
||||
|
||||
Затем `SELECT pg_try_advisory_lock(hashtext(:lock_key))`. Если `false` — `backoff`:
|
||||
|
||||
```sql
|
||||
UPDATE dl_jobs
|
||||
SET status='queued', available_at = now() + make_interval(secs => :sec)
|
||||
WHERE job_id=:jid;
|
||||
```
|
||||
|
||||
2. **Heartbeat** раз в `DL_HEARTBEAT_SEC`:
|
||||
|
||||
```sql
|
||||
UPDATE dl_jobs
|
||||
SET heartbeat_at = now(),
|
||||
lease_expires_at = now() + make_interval(secs => :ttl)
|
||||
WHERE job_id = :jid AND status='running';
|
||||
```
|
||||
|
||||
3. **Завершение**:
|
||||
|
||||
* Успех:
|
||||
|
||||
```sql
|
||||
UPDATE dl_jobs
|
||||
SET status='succeeded', finished_at=now(), lease_expires_at=NULL
|
||||
WHERE job_id=:jid;
|
||||
```
|
||||
|
||||
* Ошибка/ретрай:
|
||||
|
||||
```sql
|
||||
UPDATE dl_jobs
|
||||
SET status = CASE WHEN attempt < max_attempts THEN 'queued' ELSE 'failed' END,
|
||||
available_at = CASE WHEN attempt < max_attempts THEN now() + make_interval(secs => 30 * attempt) ELSE now() END,
|
||||
error = :err,
|
||||
lease_expires_at = NULL,
|
||||
finished_at = CASE WHEN attempt >= max_attempts THEN now() ELSE NULL END
|
||||
WHERE job_id=:jid;
|
||||
```
|
||||
|
||||
Всегда выставлять/снимать advisory-lock на `lock_key`.
|
||||
|
||||
4. **Отмена**: воркер проверяет `cancel_requested` между чанками; при `true` завершает пайплайн (обычно как `canceled` либо как `failed` без ретраев — политика проекта).
|
||||
|
||||
5. **Reaper** (фон у приложения): раз в `DL_REAPER_PERIOD_SEC` возвращает «потерянные» задачи в очередь.
|
||||
|
||||
```sql
|
||||
UPDATE dl_jobs
|
||||
SET status='queued', available_at=now(), lease_expires_at=NULL
|
||||
WHERE status='running'
|
||||
AND lease_expires_at IS NOT NULL
|
||||
AND lease_expires_at < now()
|
||||
RETURNING job_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) Оптимизация и SLA
|
||||
|
||||
* Claim — O(log N) благодаря частичному индексу `ix_dl_jobs_claim`.
|
||||
* Reaper — O(log N) по индексу `ix_dl_jobs_running_lease`.
|
||||
* `/health` — без БД; время ответа ≤ 20 мс. `/jobs/*` — не держат долгих транзакций.
|
||||
* Гарантия доставки: **at-least-once**; операции записи в целевые таблицы — идемпотентны (реализуется в конкретных пайплайнах).
|
||||
* Конкуренция: один `lock_key` одновременно исполняется одним воркером; параллелизм достигается независимыми `partition_key`.
|
||||
|
||||
---
|
||||
|
||||
## 8) Конфигурация (ENV)
|
||||
|
||||
* `DL_DB_DSN` — DSN Postgres (async).
|
||||
* `WORKERS_JSON` — JSON-список конфигураций воркеров, напр.: `[{"queue":"load.cbr","concurrency":2},{"queue":"load.sgx","concurrency":1}]`.
|
||||
* `DL_HEARTBEAT_SEC` (деф. 10), `DL_DEFAULT_LEASE_TTL_SEC` (деф. 60), `DL_REAPER_PERIOD_SEC` (деф. 10), `DL_CLAIM_BACKOFF_SEC` (деф. 15).
|
||||
* Логирование, middleware, `uvicorn_logging_config` — **из шаблона без изменения контрактов**.
|
||||
|
||||
---
|
||||
|
||||
## 9) Эксплуатация и деплой
|
||||
|
||||
* Один контейнер, один Pod, **несколько async-воркеров** внутри процесса (через `WorkerManager`).
|
||||
* Масштабирование — количеством реплик Deployment: очередь в БД, `FOR UPDATE SKIP LOCKED` и advisory-lock обеспечат корректность в гонке.
|
||||
* Пробы: `readiness/liveness` на `/health` из `os_router.py`.
|
||||
* Завершение: на SIGTERM — остановить reaper, подать сигнал воркерам для мягкой остановки, дождаться тасков с таймаутом.
|
||||
|
||||
---
|
||||
|
||||
## 10) Безопасность, аудит, наблюдаемость
|
||||
|
||||
* Структурные логи через `logger/*` шаблона; маскирование чувствительных полей — как в `logger/utils.py`.
|
||||
* Журнал жизненного цикла в `dl_job_events` (queued/picked/heartbeat/requeue/done/failed/canceled).
|
||||
* Метрики (BETA) — через `metric_router.py` из шаблона при необходимости.
|
||||
|
||||
---
|
||||
|
||||
## 11) Тест-план
|
||||
|
||||
* Интеграционные тесты `v1`: постановка → статус → отмена.
|
||||
* E2E: постановка → claim мок-воркером → heartbeat → done → статус `succeeded`.
|
||||
* Конкуренция: два воркера на один `lock_key` → один backoff, один исполняет.
|
||||
* Reaper: просроченный lease → возврат в `queued`.
|
||||
|
||||
---
|
||||
|
||||
## 12) TODO
|
||||
|
||||
* [ ] `context.py`: инициализация engine/sessionmaker, AppContext (как в шаблоне).
|
||||
* [ ] `api/v1/router.py`: `trigger`, `status`, `cancel`.
|
||||
* [ ] `api/v1/service.py`: бизнес-логика поверх репозитория.
|
||||
* [ ] `storage/repositories.py`: SQL для create_or_get, get_status, cancel, requeue_lost.
|
||||
* [ ] `workers/base.py`: claim/heartbeat/finish/retry/cancel/advisory-lock.
|
||||
* [ ] `workers/manager.py`: парсинг `WORKERS_JSON`, создание тасков, graceful shutdown.
|
||||
* [ ] Тесты `tests/integration_tests/v1_api/test_service.py` по стилю шаблона.
|
||||
* [ ] Документация `.env` и примеры `WORKERS_JSON`.
|
||||
* [x] **БД-таблицы уже созданы** (DDL применён).
|
||||
|
|
@ -1,731 +0,0 @@
|
|||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.3"
|
||||
description = "Document parameters, class attributes, return types, and variables inline, with Annotated."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580"},
|
||||
{file = "annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"},
|
||||
{file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
trio = ["trio (>=0.31.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.30.0"
|
||||
description = "An asyncio PostgreSQL driver"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"},
|
||||
{file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"},
|
||||
{file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"},
|
||||
{file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"},
|
||||
{file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"},
|
||||
{file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"},
|
||||
{file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"},
|
||||
{file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"]
|
||||
gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""]
|
||||
test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"},
|
||||
{file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.0"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
||||
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["main"]
|
||||
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 = "fastapi"
|
||||
version = "0.121.0"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106"},
|
||||
{file = "fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-doc = ">=0.0.2"
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.40.0,<0.50.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.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,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
|
||||
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"]
|
||||
files = [
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
|
||||
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
description = "Python logging made (stupidly) simple"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.5"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"},
|
||||
{file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
|
||||
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.3"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"},
|
||||
{file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.6.0"
|
||||
pydantic-core = "2.41.4"
|
||||
typing-extensions = ">=4.14.1"
|
||||
typing-inspection = ">=0.4.2"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.4"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"},
|
||||
{file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"},
|
||||
{file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"},
|
||||
{file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"},
|
||||
{file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"},
|
||||
{file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"},
|
||||
{file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"},
|
||||
{file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"},
|
||||
{file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"},
|
||||
{file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"},
|
||||
{file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"},
|
||||
{file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"},
|
||||
{file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"},
|
||||
{file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"},
|
||||
{file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"},
|
||||
{file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"},
|
||||
{file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"},
|
||||
{file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.14.1"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.11.0"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"},
|
||||
{file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.7.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
typing-inspection = ">=0.4.0"
|
||||
|
||||
[package.extras]
|
||||
aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"]
|
||||
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||
gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
||||
toml = ["tomli (>=2.0.1)"]
|
||||
yaml = ["pyyaml (>=6.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"},
|
||||
{file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
|
||||
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
description = "Database Abstraction Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-win32.whl", hash = "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74"},
|
||||
{file = "SQLAlchemy-2.0.44-cp37-cp37m-win_amd64.whl", hash = "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"},
|
||||
{file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"},
|
||||
{file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"},
|
||||
{file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"},
|
||||
{file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-win32.whl", hash = "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013"},
|
||||
{file = "sqlalchemy-2.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"},
|
||||
{file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"},
|
||||
{file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"},
|
||||
{file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
|
||||
aioodbc = ["aioodbc", "greenlet (>=1)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (>=1)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=8)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
||||
pymysql = ["pymysql"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.49.3"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f"},
|
||||
{file = "starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.6.2,<5"
|
||||
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
description = "Runtime typing introspection tools"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
|
||||
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.23.2"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"},
|
||||
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.0"
|
||||
h11 = ">=0.8"
|
||||
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
description = "A small Python utility to set file creation time on Windows"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"},
|
||||
{file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "20a5741e4fc170b6e9c9e47034bde99a2a1a34fdd85e4436699c06fb01b715fc"
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
[tool.poetry]
|
||||
name = "dataloader"
|
||||
version = "0.1.0"
|
||||
description = "Dataloader for something"
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
packages = [{include = "dataloader", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
uvicorn = "^0.23.2"
|
||||
fastapi = "^0.121.0"
|
||||
pydantic = "^2.12.3"
|
||||
pydantic-settings = "^2.11.0"
|
||||
asyncpg = "^0.30.0"
|
||||
sqlalchemy = "^2.0.0"
|
||||
httpx = "^0.28.0"
|
||||
pytz = "^2025.1"
|
||||
loguru = "^0.7.2"
|
||||
|
||||
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
dataloader = "dataloader.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
129
rest_template.md
129
rest_template.md
|
|
@ -1,129 +0,0 @@
|
|||
```bash
|
||||
aigw-project/
|
||||
└── src/
|
||||
└── tenera_etl/ # следует использовать своё название
|
||||
├── api/
|
||||
│ ├── v1/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py
|
||||
│ │ ├── schemas.py # pydantic models
|
||||
│ │ ├── service.py
|
||||
│ │ ├── models.py # db models, data classes, named tuples
|
||||
│ │ ├── exceptions.py
|
||||
│ │ └── utils.py
|
||||
│ ├── __init__.py
|
||||
│ ├── middleware.py
|
||||
│ ├── os_router.py # роутер для health и status endpoints
|
||||
│ ├── metric_router.py # !BETA! роутер для оценки сервиса
|
||||
│ └── schemas.py # валидация endpoint-ов из os_router и metric_router
|
||||
├── interfaces/ # интерфейсы для подключения к внешним AC
|
||||
├── logger/
|
||||
│ ├── __init__.py
|
||||
│ ├── context_vars.py # управление контекстом запросов для логирования
|
||||
│ ├── logger.py # реализация логгера
|
||||
│ ├── models.py # модели логов, метрик, событий аудита
|
||||
│ ├── utils.py # функции + маскирование args
|
||||
│ └── uvicorn_logging_config.py # конфигурация логирования uvicorn
|
||||
├── __main__.py
|
||||
├── base.py
|
||||
├── config.py # global configs
|
||||
├── exceptions.py # global exceptions
|
||||
└── context.py
|
||||
|
||||
├── tests/
|
||||
│ └── integration_tests/
|
||||
│ ├── fixtures/
|
||||
│ │ └── fixture_*.py
|
||||
│ ├── conftest.py
|
||||
│ ├── gigachat_interface/ # тесты интерфейсов (на GigaController)
|
||||
│ │ └── test_gptchat_*.py
|
||||
│ └── v1_api/
|
||||
│ ├── constants.py
|
||||
│ ├── test_service.py # тесты бизнес-логики
|
||||
│ └── test_service_*.py
|
||||
|
||||
├── app.sh # запуск в OpenShift, содержит tool.poetry.scripts
|
||||
├── Dockerfile # основной Dockerfile
|
||||
├── Dockerfile-k8s # локальный Docker
|
||||
├── pyproject.toml
|
||||
├── poetry.lock
|
||||
├── .env # переменные окружения
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
|
||||
## 📁 Назначение ключевых файлов
|
||||
|
||||
### `src/tenera_etl/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|--------------------------|------------|
|
||||
| `__main__.py` | Точка входа. Запускает FastAPI-приложение. |
|
||||
| `config.py` | Загрузка и обработка переменных окружения. |
|
||||
| `base.py` | Базовые классы и типы, переиспользуемые в проекте. |
|
||||
| `context.py` | Реализация паттерна `AppContext` — единая точка доступа к зависимостям. |
|
||||
|
||||
---
|
||||
|
||||
### `src/tenera_etl/logger/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|------------------------------|------------|
|
||||
| `context_vars.py` | Контекстные переменные для логирования (`Request-ID` и др.). ⚠️ Не редактировать.|
|
||||
| `logger.py` | Основной логгер приложения. ⚠️ Не редактировать. |
|
||||
| `models.py` | Модели логов, метрик, аудита. ⚠️ Не редактировать. |
|
||||
| `utils.py` | Утилиты логгера, включая маскирование данных. |
|
||||
| `uvicorn_logging_config.py` | Конфигурация логгирования `uvicorn`. |
|
||||
|
||||
---
|
||||
|
||||
### `src/tenera_etl/api/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|--------------------|------------|
|
||||
| `__init__.py` | Конфигуратор FastAPI — регистрация версий и роутов. |
|
||||
| `os_router.py` | Инфраструктурные endpoint'ы (`/health`, `/status`). ⚠️ Не редактировать. |
|
||||
| `metric_router.py` | Метрики (BETA). ⚠️ Не редактировать. |
|
||||
| `schemas.py` | Схемы (Pydantic) для `os_router` и `metric_router`. ⚠️ Не редактировать. |
|
||||
| `middleware.py` | Мидлвар для логирования входящих и исходящих запросов. ⚠️ Не редактировать. |
|
||||
|
||||
---
|
||||
|
||||
### `src/tenera_etl/api/v1/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|------------------|------------|
|
||||
| `router.py` | Основные endpoint'ы бизнес-логики (`v1`). |
|
||||
| `schemas.py` | Pydantic-схемы запросов и ответов. |
|
||||
| `models.py` | Модели данных (DB, классы, namedtuples). |
|
||||
| `service.py` | Реализация бизнес-логики. |
|
||||
| `utils.py` | Утилиты и вспомогательные функции. |
|
||||
| `exceptions.py` | Исключения (`RestNotFound`, `InvalidUserData`, и др.). |
|
||||
|
||||
---
|
||||
|
||||
### `src/tenera_etl/interfaces/`
|
||||
|
||||
| Файл / Папка | Назначение |
|
||||
|--------------|------------|
|
||||
| *всё содержимое* | Интерфейсы взаимодействия с внешними системами (АС и пр.). |
|
||||
|
||||
|
||||
Пример логирования в коде:
|
||||
|
||||
```python
|
||||
from tenera_etl.logger import logger
|
||||
|
||||
logger.info("End processing user registration request")
|
||||
```
|
||||
⚠️ Не передавайте в logger.info(...) ничего, кроме строки — она будет записана в поле message.
|
||||
|
||||
Маскирование чувствительных данных
|
||||
|
||||
В файле logger/utils.py реализовано маскирование:
|
||||
|
||||
все поля, содержащие ключевые слова вроде password, token, secret, будут скрыты;
|
||||
|
||||
работает автоматически, но вы можете конфигурировать список слов и правила.
|
||||
|
||||
Перед добавлением кастомной маскировки — ознакомьтесь с документацией, чтобы избежать утечки данных.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"""dataloader package exports.
|
||||
|
||||
Собирает верхнеуровневые сабмодули для удобных импортов.
|
||||
"""
|
||||
|
||||
from . import api
|
||||
|
||||
|
||||
__all__ = [
|
||||
"api",
|
||||
]
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import uvicorn
|
||||
|
||||
|
||||
from dataloader.api import app_main
|
||||
from dataloader.config import APP_CONFIG
|
||||
from dataloader.logger.uvicorn_logging_config import LOGGING_CONFIG, setup_uvicorn_logging
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Инициализируем логирование uvicorn перед запуском
|
||||
setup_uvicorn_logging()
|
||||
|
||||
uvicorn.run(
|
||||
app_main,
|
||||
host=APP_CONFIG.app.app_host,
|
||||
port=APP_CONFIG.app.app_port,
|
||||
access_log=False,
|
||||
log_config=LOGGING_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
import typing as tp
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from .metric_router import router as metric_router
|
||||
from .middleware import log_requests
|
||||
from .os_router import router as service_router
|
||||
from .v1 import router as v1_router
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: tp.Any) -> AsyncGenerator[None, None]:
|
||||
from dataloader.context import APP_CTX
|
||||
|
||||
await APP_CTX.on_startup()
|
||||
yield
|
||||
await APP_CTX.on_shutdown()
|
||||
|
||||
|
||||
app_main = FastAPI(title="Data Gateway", lifespan=lifespan)
|
||||
|
||||
app_main.middleware("http")(log_requests)
|
||||
|
||||
app_main.include_router(
|
||||
service_router, tags=["Openshift dataloader routes"]
|
||||
)
|
||||
app_main.include_router(
|
||||
metric_router, tags=["Like/dislike metric dataloader routes"]
|
||||
)
|
||||
app_main.include_router(
|
||||
v1_router, prefix="/api/v1", tags=["dataloader"]
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"app_main",
|
||||
]
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
""" 🚨 НЕ РЕДАКТИРОВАТЬ !!!!!!
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Header, status
|
||||
from dataloader.context import APP_CTX
|
||||
from . import schemas
|
||||
|
||||
router = APIRouter()
|
||||
logger = APP_CTX.get_logger()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/like",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=schemas.RateResponse,
|
||||
)
|
||||
async def like(
|
||||
# pylint: disable=C0103,W0613
|
||||
header_Request_Id: str = Header(uuid.uuid4(), alias="Request-Id")
|
||||
) -> dict[str, str]:
|
||||
logger.metric(
|
||||
metric_name="dataloader_likes_total",
|
||||
metric_value=1,
|
||||
)
|
||||
return {"rating_result": "like recorded"}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/dislike",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=schemas.RateResponse,
|
||||
)
|
||||
async def dislike(
|
||||
# pylint: disable=C0103,W0613
|
||||
header_Request_Id: str = Header(uuid.uuid4(), alias="Request-Id")
|
||||
) -> dict[str, str]:
|
||||
logger.metric(
|
||||
metric_name="dataloader_dislikes_total",
|
||||
metric_value=1,
|
||||
)
|
||||
return {"rating_result": "dislike recorded"}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Request, status
|
||||
from starlette.concurrency import iterate_in_threadpool
|
||||
|
||||
from dataloader.context import APP_CTX
|
||||
|
||||
NON_LOGGED_ENDPOINTS = (
|
||||
"/like",
|
||||
"/dislike",
|
||||
"/health",
|
||||
"/openapi.json",
|
||||
"/docs",
|
||||
)
|
||||
|
||||
HEADERS_WHITE_LIST_TO_LOG = (
|
||||
"Request-Id",
|
||||
"Request-Time",
|
||||
"System-Id",
|
||||
"GateWay-Session-Id",
|
||||
"Client-Id",
|
||||
)
|
||||
|
||||
|
||||
def _get_decoded_body(raw_body: bytes, message_type: str, logger) -> dict:
|
||||
decoded_body = {}
|
||||
try:
|
||||
decoded_body = json.loads(raw_body.decode())
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
logger.warning(f"{message_type} body is not json")
|
||||
return decoded_body
|
||||
|
||||
|
||||
async def log_requests(request: Request, call_next) -> any:
|
||||
start_time = time.time()
|
||||
logger = APP_CTX.get_logger()
|
||||
request_path = request.url.path
|
||||
|
||||
allowed_headers_to_log = ((k, request.headers.get(k)) for k in HEADERS_WHITE_LIST_TO_LOG)
|
||||
headers_to_log = {header_name: header_value for header_name, header_value in allowed_headers_to_log if header_value}
|
||||
|
||||
APP_CTX.get_context_vars_container().set_context_vars(
|
||||
request_id=headers_to_log.get("Request-Id", ""),
|
||||
request_time=headers_to_log.get("Request-Time", ""),
|
||||
system_id=headers_to_log.get("System-Id", ""),
|
||||
gw_session_id=headers_to_log.get("GateWay-Session-Id", ""),
|
||||
)
|
||||
|
||||
if request_path in NON_LOGGED_ENDPOINTS:
|
||||
response = await call_next(request)
|
||||
logger.debug(f"Processed request for {request_path} with code {response.status_code}")
|
||||
elif headers_to_log.get("Request-Id", None):
|
||||
raw_request_body = await request.body()
|
||||
request_body_decoded = _get_decoded_body(raw_request_body, "request", logger)
|
||||
|
||||
logger.info(
|
||||
f"Incoming {request.method}-request for {request_path}",
|
||||
args={
|
||||
"headers": headers_to_log,
|
||||
"message": request_body_decoded,
|
||||
},
|
||||
message_type="request",
|
||||
path=request_path,
|
||||
)
|
||||
|
||||
client_id = headers_to_log.get("Client-Id", None)
|
||||
if client_id:
|
||||
logger.metric(
|
||||
metric_name=f"dataloader_user_{client_id}",
|
||||
metric_value=1,
|
||||
)
|
||||
logger.metric(
|
||||
metric_name="dataloader_requests_total",
|
||||
metric_value=1,
|
||||
)
|
||||
logger.audit(
|
||||
event_name="BusinessRequestReceived",
|
||||
event_params=json.dumps(
|
||||
request_body_decoded,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
response_body = [chunk async for chunk in response.body_iterator]
|
||||
response.body_iterator = iterate_in_threadpool(iter(response_body))
|
||||
|
||||
headers_to_log["Response-Time"] = datetime.now(APP_CTX.get_pytz_timezone()).isoformat()
|
||||
for header in headers_to_log:
|
||||
response.headers[header] = headers_to_log[header]
|
||||
|
||||
response_body_extracted = response_body[0] if len(response_body) > 0 else b""
|
||||
decoded_response_body = _get_decoded_body(response_body_extracted, "response", logger)
|
||||
|
||||
logger.info(
|
||||
"Outgoing response to client system",
|
||||
args={
|
||||
"headers": headers_to_log,
|
||||
"message": decoded_response_body,
|
||||
},
|
||||
message_type="response",
|
||||
path=request_path,
|
||||
)
|
||||
|
||||
logger.metric(
|
||||
metric_name="dataloader_responses_total",
|
||||
metric_value=1,
|
||||
)
|
||||
|
||||
logger.audit(
|
||||
event_name="BusinessRequestFinished",
|
||||
event_params=json.dumps(
|
||||
decoded_response_body,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
processing_time_ms = int(round((time.time() - start_time), 3) * 1000)
|
||||
logger.info(f"Request processing time for {request_path}: {processing_time_ms} ms")
|
||||
logger.metric(
|
||||
metric_name="dataloader_process_duration_ms",
|
||||
metric_value=processing_time_ms,
|
||||
)
|
||||
|
||||
if response.status_code < status.HTTP_400_BAD_REQUEST:
|
||||
logger.metric(
|
||||
metric_name="dataloader_request_status_success_total",
|
||||
metric_value=1,
|
||||
)
|
||||
else:
|
||||
logger.metric(
|
||||
metric_name="dataloader_request_status_failure_total",
|
||||
metric_value=1,
|
||||
)
|
||||
else:
|
||||
logger.info(f"Incoming {request.method}-request with no id for {request_path}")
|
||||
response = await call_next(request)
|
||||
logger.info(f"Request with no id for {request_path} processing time: {time.time() - start_time:.3f} s")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
__all__ = [
|
||||
"log_requests",
|
||||
]
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Инфраструктурные endpoint'ы (/health, /status)
|
||||
""" 🚨 НЕ РЕДАКТИРОВАТЬ !!!!!!
|
||||
"""
|
||||
|
||||
from importlib.metadata import distribution
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from . import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=schemas.HealthResponse,
|
||||
)
|
||||
async def health() -> dict[str, str]:
|
||||
return {
|
||||
"health_status": "running",
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/info",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=schemas.InfoResponse,
|
||||
)
|
||||
async def info() -> schemas.InfoResponse:
|
||||
dist = distribution("aigw-rest-service")
|
||||
|
||||
return schemas.InfoResponse(
|
||||
name=str(dist.metadata["Name"]),
|
||||
description=str(dist.metadata["Summary"]),
|
||||
type="REST API",
|
||||
version=str(dist.version),
|
||||
)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Ответ для ручки /health"""
|
||||
status: str = Field(default="running", description="Service health check", max_length=7)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {"example": {"status": "running"}}
|
||||
|
||||
|
||||
class InfoResponse(BaseModel):
|
||||
"""Ответ для ручки /info"""
|
||||
name: str = Field(description="Service name", max_length=50)
|
||||
description: str = Field(description="Service description", max_length=200)
|
||||
type: str = Field(default="REST API", description="Service type", max_length=20)
|
||||
version: str = Field(description="Service version", max_length=20, pattern=r"^\d+\.\d+\.\d+")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "rest-template",
|
||||
"description": "Python 'AI gateway' template for developing REST microservices",
|
||||
"type": "REST API",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RateResponse(BaseModel):
|
||||
rating_result: str = Field(description="Rating that was recorded", max_length=50)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from .router import router
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""Агрегатор v1-роутов.
|
||||
|
||||
Экспортирует готовый `router`, собранный из модульных роутеров в пакете `routes`.
|
||||
Оставлен как тонкий слой для обратной совместимости импортов `from dataloader.api.v1 import router`.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs) -> Any:
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import os
|
||||
from logging import DEBUG, INFO
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class BaseAppSettings(BaseSettings):
|
||||
"""
|
||||
Базовый класс для настроек.
|
||||
"""
|
||||
local: bool = Field(validation_alias="LOCAL", default=False)
|
||||
debug: bool = Field(validation_alias="DEBUG", default=False)
|
||||
|
||||
@property
|
||||
def protocol(self) -> str:
|
||||
return "https" if self.local else "http"
|
||||
|
||||
|
||||
class AppSettings(BaseAppSettings):
|
||||
"""
|
||||
Настройки приложения.
|
||||
"""
|
||||
app_host: str = Field(validation_alias="APP_HOST", default="0.0.0.0")
|
||||
app_port: int = Field(validation_alias="APP_PORT", default=8081)
|
||||
kube_net_name: str = Field(validation_alias="PROJECT_NAME", default="AIGATEWAY")
|
||||
timezone: str = Field(validation_alias="TIMEZONE", default="Europe/Moscow")
|
||||
|
||||
|
||||
class LogSettings(BaseAppSettings):
|
||||
"""
|
||||
Настройки логирования.
|
||||
"""
|
||||
private_log_file_path: str = Field(validation_alias="LOG_PATH", default=os.getcwd())
|
||||
private_log_file_name: str = Field(validation_alias="LOG_FILE_NAME", default="app.log")
|
||||
log_rotation: str = Field(validation_alias="LOG_ROTATION", default="10 MB")
|
||||
private_metric_file_path: str = Field(validation_alias="METRIC_PATH", default=os.getcwd())
|
||||
private_metric_file_name: str = Field(validation_alias="METRIC_FILE_NAME", default="app-metric.log")
|
||||
private_audit_file_path: str = Field(validation_alias="AUDIT_LOG_PATH", default=os.getcwd())
|
||||
private_audit_file_name: str = Field(validation_alias="AUDIT_LOG_FILE_NAME", default="events.log")
|
||||
audit_host_ip: str = Field(validation_alias="HOST_IP", default="127.0.0.1")
|
||||
audit_host_uid: str = Field(validation_alias="HOST_UID", default="63b6dcee-170b-49bf-a65c-3ec967398ccd")
|
||||
|
||||
@staticmethod
|
||||
def get_file_abs_path(path_name: str, file_name: str) -> str:
|
||||
return os.path.join(path_name.strip("/"), file_name.lstrip("/")).strip()
|
||||
|
||||
@property
|
||||
def log_file_abs_path(self) -> str:
|
||||
return self.get_file_abs_path(self.private_log_file_path, self.private_log_file_name)
|
||||
|
||||
@property
|
||||
def metric_file_abs_path(self) -> str:
|
||||
return self.get_file_abs_path(self.private_metric_file_path, self.private_metric_file_name)
|
||||
|
||||
@property
|
||||
def audit_file_abs_path(self) -> str:
|
||||
return self.get_file_abs_path(self.private_audit_file_path, self.private_audit_file_name)
|
||||
|
||||
@property
|
||||
def log_lvl(self) -> int:
|
||||
return DEBUG if self.debug else INFO
|
||||
|
||||
|
||||
class PGSettings(BaseSettings):
|
||||
host: str = Field(validation_alias="PG_HOST", default="localhost")
|
||||
port: int = Field(validation_alias="PG_PORT", default=5432)
|
||||
user: str = Field(validation_alias="PG_USER", default="postgres")
|
||||
password: str = Field(validation_alias="PG_PASSWORD", default="")
|
||||
database: str = Field(validation_alias="PG_DATABASE", default="postgres")
|
||||
schema_: str = Field(validation_alias="PG_SCHEMA", default="public")
|
||||
use_pool: bool = Field(validation_alias="PG_USE_POOL", default=True)
|
||||
pool_size: int = Field(validation_alias="PG_POOL_SIZE", default=5)
|
||||
max_overflow: int = Field(validation_alias="PG_MAX_OVERFLOW", default=10)
|
||||
pool_recycle: int = Field(validation_alias="PG_POOL_RECYCLE", default=1800)
|
||||
connect_timeout: int = Field(validation_alias="PG_CONNECT_TIMEOUT", default=10)
|
||||
command_timeout: int = Field(validation_alias="PG_COMMAND_TIMEOUT", default=60)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Автоматически генерируется SQLAlchemy URL для подключения"""
|
||||
return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
|
||||
|
||||
|
||||
class Secrets:
|
||||
"""
|
||||
Класс, агрегирующий все настройки приложения.
|
||||
"""
|
||||
app: AppSettings = AppSettings()
|
||||
log: LogSettings = LogSettings()
|
||||
pg: PGSettings = PGSettings()
|
||||
|
||||
|
||||
APP_CONFIG = Secrets()
|
||||
|
||||
__all__ = [
|
||||
"Secrets",
|
||||
"APP_CONFIG",
|
||||
]
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# Реализация паттерна AppContext — единая точка доступа к зависимостям
|
||||
from dataloader.base import Singleton
|
||||
import typing
|
||||
from dataloader.config import APP_CONFIG, Secrets
|
||||
from dataloader.logger import ContextVarsContainer, LoggerConfigurator
|
||||
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
|
||||
|
||||
class AppContext(metaclass=Singleton):
|
||||
@property
|
||||
def logger(self) -> "typing.Any":
|
||||
return self._logger_manager.async_logger
|
||||
|
||||
|
||||
def __init__(self, secrets: Secrets) -> None:
|
||||
self.timezone = pytz.timezone(secrets.app.timezone)
|
||||
self.context_vars_container = ContextVarsContainer()
|
||||
self._logger_manager = LoggerConfigurator(
|
||||
log_lvl=secrets.log.log_lvl,
|
||||
log_file_path=secrets.log.log_file_abs_path,
|
||||
metric_file_path=secrets.log.metric_file_abs_path,
|
||||
audit_file_path=secrets.log.audit_file_abs_path,
|
||||
audit_host_ip=secrets.log.audit_host_ip,
|
||||
audit_host_uid=secrets.log.audit_host_uid,
|
||||
context_vars_container=self.context_vars_container,
|
||||
timezone=self.timezone,
|
||||
)
|
||||
self.pg = secrets.pg
|
||||
self.logger.info("App context initialized.")
|
||||
|
||||
|
||||
def get_logger(self) -> "typing.Any":
|
||||
return self.logger
|
||||
|
||||
|
||||
def get_context_vars_container(self) -> ContextVarsContainer:
|
||||
return self.context_vars_container
|
||||
|
||||
|
||||
def get_pytz_timezone(self):
|
||||
return self.timezone
|
||||
|
||||
|
||||
async def on_startup(self) -> None:
|
||||
self.logger.info("Application is starting up.")
|
||||
self.logger.info("All connections checked. Application is up and ready.")
|
||||
|
||||
|
||||
async def on_shutdown(self) -> None:
|
||||
self.logger.info("Application is shutting down.")
|
||||
self._logger_manager.remove_logger_handlers()
|
||||
|
||||
|
||||
|
||||
APP_CTX = AppContext(APP_CONFIG)
|
||||
|
||||
|
||||
__all__ = ["APP_CTX"]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# logger package
|
||||
from .context_vars import ContextVarsContainer
|
||||
from .logger import LoggerConfigurator
|
||||
|
||||
__all__ = ["ContextVarsContainer", "LoggerConfigurator"]
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# Управление контекстом запросов для логирования
|
||||
import uuid
|
||||
from contextvars import ContextVar
|
||||
from typing import Final
|
||||
|
||||
|
||||
REQUEST_ID_CTX_VAR: Final[ContextVar[str]] = ContextVar("request_id", default="")
|
||||
DEVICE_ID_CTX_VAR: Final[ContextVar[str]] = ContextVar("device_id", default="")
|
||||
SESSION_ID_CTX_VAR: Final[ContextVar[str]] = ContextVar("session_id", default="")
|
||||
REQUEST_TIME_CTX_VAR: Final[ContextVar[str]] = ContextVar("request_time", default="")
|
||||
SYSTEM_ID_CTX_VAR: Final[ContextVar[str]] = ContextVar("system_id", default="")
|
||||
GW_SESSION_ID_CTX_VAR: Final[ContextVar[str]] = ContextVar("gw_session_id", default="")
|
||||
|
||||
|
||||
class ContextVarsContainer:
|
||||
@property
|
||||
def request_id(self) -> str:
|
||||
if not (request_id := REQUEST_ID_CTX_VAR.get()):
|
||||
request_id = str(uuid.uuid4())
|
||||
REQUEST_ID_CTX_VAR.set(request_id)
|
||||
return request_id
|
||||
|
||||
def set_request_id(self, request_id: str = "") -> str:
|
||||
if not request_id:
|
||||
request_id = str(uuid.uuid4())
|
||||
REQUEST_ID_CTX_VAR.set(request_id)
|
||||
return request_id
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
return DEVICE_ID_CTX_VAR.get()
|
||||
|
||||
@device_id.setter
|
||||
def device_id(self, value: str) -> None:
|
||||
DEVICE_ID_CTX_VAR.set(value)
|
||||
|
||||
@property
|
||||
def session_id(self) -> str:
|
||||
return SESSION_ID_CTX_VAR.get()
|
||||
|
||||
@session_id.setter
|
||||
def session_id(self, value: str) -> None:
|
||||
SESSION_ID_CTX_VAR.set(value)
|
||||
|
||||
@property
|
||||
def request_time(self) -> str:
|
||||
return REQUEST_TIME_CTX_VAR.get()
|
||||
|
||||
@request_time.setter
|
||||
def request_time(self, value: str) -> None:
|
||||
REQUEST_TIME_CTX_VAR.set(value)
|
||||
|
||||
@property
|
||||
def system_id(self) -> str:
|
||||
return SYSTEM_ID_CTX_VAR.get()
|
||||
|
||||
@system_id.setter
|
||||
def system_id(self, value: str) -> None:
|
||||
SYSTEM_ID_CTX_VAR.set(value)
|
||||
|
||||
@property
|
||||
def gw_session_id(self) -> str:
|
||||
return GW_SESSION_ID_CTX_VAR.get()
|
||||
|
||||
@gw_session_id.setter
|
||||
def gw_session_id(self, value: str) -> None:
|
||||
GW_SESSION_ID_CTX_VAR.set(value)
|
||||
|
||||
def set_context_vars(self, request_id: str = "", request_time: str = "", system_id: str = "", gw_session_id: str = "") -> None:
|
||||
if request_id:
|
||||
self.set_request_id(request_id)
|
||||
if request_time:
|
||||
self.request_time = request_time
|
||||
if system_id:
|
||||
self.system_id = system_id
|
||||
if gw_session_id:
|
||||
self.gw_session_id = gw_session_id
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
"request_id": self.request_id,
|
||||
"device_id": self.device_id,
|
||||
"session_id": self.session_id,
|
||||
"request_time": self.request_time,
|
||||
"system_id": self.system_id,
|
||||
"gw_session_id": self.gw_session_id,
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
# Основной логгер приложения
|
||||
import sys
|
||||
import typing
|
||||
from datetime import tzinfo
|
||||
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
from .context_vars import ContextVarsContainer
|
||||
|
||||
|
||||
|
||||
# Определяем фильтры для разных типов логов
|
||||
def metric_only_filter(record: dict) -> bool:
|
||||
return "metric" in record["extra"]
|
||||
|
||||
|
||||
|
||||
def audit_only_filter(record: dict) -> bool:
|
||||
return "audit" in record["extra"]
|
||||
|
||||
|
||||
|
||||
def regular_log_filter(record: dict) -> bool:
|
||||
return "metric" not in record["extra"] and "audit" not in record["extra"]
|
||||
|
||||
|
||||
|
||||
class LoggerConfigurator:
|
||||
def __init__(
|
||||
self,
|
||||
log_lvl: str,
|
||||
log_file_path: str,
|
||||
metric_file_path: str,
|
||||
audit_file_path: str,
|
||||
audit_host_ip: str,
|
||||
audit_host_uid: str,
|
||||
context_vars_container: ContextVarsContainer,
|
||||
timezone: tzinfo,
|
||||
) -> None:
|
||||
self.context_vars_container = context_vars_container
|
||||
self.timezone = timezone
|
||||
self.log_lvl = log_lvl
|
||||
self.log_file_path = log_file_path
|
||||
self.metric_file_path = metric_file_path
|
||||
self.audit_file_path = audit_file_path
|
||||
self.audit_host_ip = audit_host_ip
|
||||
self.audit_host_uid = audit_host_uid
|
||||
self._handler_ids = []
|
||||
self.configure_logger()
|
||||
|
||||
|
||||
@property
|
||||
def async_logger(self) -> "typing.Any":
|
||||
return self._async_logger
|
||||
|
||||
|
||||
def patch_record_with_context(self, record: dict) -> None:
|
||||
context_data = self.context_vars_container.as_dict()
|
||||
record["extra"].update(context_data)
|
||||
if not record["extra"].get("request_id"):
|
||||
record["extra"]["request_id"] = "system_event"
|
||||
|
||||
|
||||
def configure_logger(self) -> None:
|
||||
"""Настройка логгера `loguru` с необходимыми обработчиками."""
|
||||
logger.remove()
|
||||
logger.patch(self.patch_record_with_context)
|
||||
|
||||
# Функция для безопасного форматирования консольных логов
|
||||
def console_format(record):
|
||||
request_id = record["extra"].get("request_id", "system_event")
|
||||
elapsed = record["elapsed"]
|
||||
level = record["level"].name
|
||||
name = record["name"]
|
||||
function = record["function"]
|
||||
line = record["line"]
|
||||
message = record["message"]
|
||||
time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
|
||||
return (
|
||||
f"<green>{time_str} ({elapsed})</green> | "
|
||||
f"<cyan>{request_id}</cyan> | "
|
||||
f"<level>{level: <8}</level> | "
|
||||
f"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||
f"<level>{message}</level>\n"
|
||||
)
|
||||
|
||||
# Обработчик для обычных логов (консоль)
|
||||
handler_id = logger.add(
|
||||
sys.stdout,
|
||||
level=self.log_lvl,
|
||||
filter=regular_log_filter,
|
||||
format=console_format,
|
||||
colorize=True,
|
||||
)
|
||||
self._handler_ids.append(handler_id)
|
||||
|
||||
|
||||
# Обработчик для обычных логов (файл)
|
||||
handler_id = logger.add(
|
||||
self.log_file_path,
|
||||
level=self.log_lvl,
|
||||
filter=regular_log_filter,
|
||||
rotation="10 MB",
|
||||
compression="zip",
|
||||
enqueue=True,
|
||||
serialize=True,
|
||||
)
|
||||
self._handler_ids.append(handler_id)
|
||||
|
||||
|
||||
# Обработчик для метрик
|
||||
handler_id = logger.add(
|
||||
self.metric_file_path,
|
||||
level="INFO",
|
||||
filter=metric_only_filter,
|
||||
rotation="10 MB",
|
||||
compression="zip",
|
||||
enqueue=True,
|
||||
serialize=True,
|
||||
)
|
||||
self._handler_ids.append(handler_id)
|
||||
|
||||
|
||||
# Обработчик для аудита
|
||||
handler_id = logger.add(
|
||||
self.audit_file_path,
|
||||
level="INFO",
|
||||
filter=audit_only_filter,
|
||||
rotation="10 MB",
|
||||
compression="zip",
|
||||
enqueue=True,
|
||||
serialize=True,
|
||||
)
|
||||
self._handler_ids.append(handler_id)
|
||||
|
||||
|
||||
self._async_logger = logger
|
||||
|
||||
|
||||
def remove_logger_handlers(self) -> None:
|
||||
"""Удаление всех обработчиков логгера."""
|
||||
for handler_id in self._handler_ids:
|
||||
self._async_logger.remove(handler_id)
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Модели логов, метрик, событий аудита
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Функции + маскирование args
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# Конфигурация логирования uvicorn
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# Get corresponding Loguru level if it exists
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||
level, record.getMessage()
|
||||
)
|
||||
|
||||
|
||||
def setup_uvicorn_logging() -> None:
|
||||
# Set all uvicorn loggers to use InterceptHandler
|
||||
for logger_name in ["uvicorn", "uvicorn.error", "uvicorn.access"]:
|
||||
log = logging.getLogger(logger_name)
|
||||
log.handlers = [InterceptHandler()]
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.propagate = False
|
||||
|
||||
|
||||
# uvicorn logging config
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"default": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {
|
||||
"handlers": ["default"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"handlers": ["default"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["default"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue