Switch observability to passive collection: remove OTLP push, Vector replaces otel-collector

- core/telemetry.py is now a no-op (no opentelemetry imports); entrypoints
  no longer call setup/shutdown telemetry
- Remove all opentelemetry-* deps from pyproject groups; regenerate uv.lock
- Drop otel_exporter_otlp_endpoint/otel_service_name settings; sentry and
  auth use "contract-check" instead
- Stop publishing worker metrics ports; bind API metrics to 127.0.0.1 by
  default via API_METRICS_BIND_HOST
- Replace otel-collector with Vector in observer profile (docker_logs +
  prometheus_scrape -> OpenObserve); add deploy/observability/vector-config.yaml
- Update .env.example, docs (ARCHITECTURE, DEPLOY), README, Makefile
- Rewrite tests/unit/test_telemetry.py for the no-op implementation
This commit is contained in:
febux 2026-09-06 19:17:08 +03:00
parent dc653737e0
commit b279d6c61a
25 changed files with 364 additions and 904 deletions

View file

@ -48,22 +48,18 @@ REFUND_FULL_USAGE_THRESHOLD=0.20 # ≤20% consumed + within window ⇒ full ref
# --- Observability (leave empty to disable) --- # --- Observability (leave empty to disable) ---
SENTRY_DSN= SENTRY_DSN=
# OTLP endpoint for traces/metrics/logs. # Auth header for OpenObserve. Used by Vector in the `observer` profile to forward
# profile: observer → http://otel-collector:4318 (OTLP/HTTP via collector) # logs and metrics. Generate with: echo -n 'user:pass' | base64
# profile: obs → configure your own otel-collector/tempo, or leave empty
OTEL_EXPORTER_OTLP_ENDPOINT=
# Auth header for OpenObserve. Used by the otel-collector to forward telemetry.
# Generate with: echo -n 'user:pass' | base64
# Default value is for root@example.com:Complexpass#123. # Default value is for root@example.com:Complexpass#123.
OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM= OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
# Only needed if sending OTLP directly to OpenObserve (without collector). # OTEL_EXPORTER_OTLP_ENDPOINT is no longer used. The application does not push
# The collector uses OPENOBSERVE_AUTH_TOKEN instead. # OTLP; Vector/OpenObserve collects logs/metrics passively from stdout and
OTEL_EXPORTER_OTLP_HEADERS= # /metrics endpoints. Remove this line from existing .env files.
OTEL_SERVICE_NAME=contract-check # OTEL_EXPORTER_OTLP_ENDPOINT=
# OTEL_EXPORTER_OTLP_HEADERS=
# --- OpenObserve (profile: observer) --- # --- OpenObserve (profile: observer) ---
OPENOBSERVE_PORT=5080 OPENOBSERVE_PORT=5080
OPENOBSERVE_GRPC_PORT=5081
OPENOBSERVE_ROOT_USER_EMAIL=root@example.com OPENOBSERVE_ROOT_USER_EMAIL=root@example.com
OPENOBSERVE_ROOT_USER_PASSWORD=Complexpass#123 OPENOBSERVE_ROOT_USER_PASSWORD=Complexpass#123
# disk | s3 (s3 requires ZO_S3_* env vars; see OpenObserve docs) # disk | s3 (s3 requires ZO_S3_* env vars; see OpenObserve docs)
@ -77,11 +73,6 @@ OPENOBSERVE_MEMORY_LIMIT=2G
OPENOBSERVE_CPU_RESERVATION=0.5 OPENOBSERVE_CPU_RESERVATION=0.5
OPENOBSERVE_MEMORY_RESERVATION=512M OPENOBSERVE_MEMORY_RESERVATION=512M
# --- OTel Collector (profile: observer) ---
OTEL_COLLECTOR_OTLP_GRPC_PORT=4317
OTEL_COLLECTOR_OTLP_HTTP_PORT=4318
OTEL_COLLECTOR_METRICS_PORT=8889
# --- Grafana / Loki (profile: obs) --- # --- Grafana / Loki (profile: obs) ---
GRAFANA_PORT=3000 GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=admin GRAFANA_ADMIN_USER=admin
@ -120,9 +111,16 @@ CHUNK_SIZE_CHARS=10000
# --- API (FastAPI) --- # --- API (FastAPI) ---
API_HOST=0.0.0.0 API_HOST=0.0.0.0
API_PORT=8000 API_PORT=8000
# Bind the /metrics endpoint to a host-local address by default so it is not
# exposed on the public interface. Set to 0.0.0.0 only when an external scraper
# (e.g. a separate monitoring host) legitimately needs access.
API_METRICS_BIND_HOST=127.0.0.1
API_METRICS_PORT=9100 API_METRICS_PORT=9100
B2B_DEFAULT_RATE_LIMIT_RPS=3 # per API key; mirrors Ollama Pro concurrency B2B_DEFAULT_RATE_LIMIT_RPS=3 # per API key; mirrors Ollama Pro concurrency
# Bearer token protecting /metrics. Leave empty to keep the endpoint open (default). # Bearer token protecting /metrics. Generate a strong secret for production and
# share it with authorized scrapers. Leaving this empty keeps /metrics open, which
# is convenient for local development but should not be used in production.
# Generate with: openssl rand -hex 32
METRICS_BEARER_TOKEN= METRICS_BEARER_TOKEN=
CORS_ORIGINS= # comma-separated, future web SPA CORS_ORIGINS= # comma-separated, future web SPA

View file

@ -132,13 +132,13 @@ obs-url: ## Print Grafana URL and default credentials
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Docker: OpenObserve collector stack # Docker: OpenObserve collector stack
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
observer-up: ## Start OpenObserve + otel-collector containers observer-up: ## Start OpenObserve + Vector containers
docker compose --profile observer up -d --build --remove-orphans docker compose --profile observer up -d --build --remove-orphans
observer-down: ## Stop OpenObserve + otel-collector containers observer-down: ## Stop OpenObserve + Vector containers
docker compose --profile observer down docker compose --profile observer down
observer-logs: ## Tail OpenObserve + otel-collector containers logs observer-logs: ## Tail OpenObserve + Vector containers logs
docker compose --profile observer logs -f docker compose --profile observer logs -f
obs-reset: ## Reset Grafana and Loki volumes (wipes dashboards/logs data) obs-reset: ## Reset Grafana and Loki volumes (wipes dashboards/logs data)

View file

@ -143,11 +143,12 @@ docker compose --profile bot up -d --build # + bot (можно запус
- `services` — api + worker-ы (без бота). - `services` — api + worker-ы (без бота).
- `bot` — Telegram-бот; можно поднять на этом же хосте или на отдельном сервере (`docs/DEPLOY.md` §14). - `bot` — Telegram-бот; можно поднять на этом же хосте или на отдельном сервере (`docs/DEPLOY.md` §14).
- `edge` — Nginx + certbot (`deploy/nginx/`, `docs/DEPLOY.md` §13). - `edge` — Nginx + certbot (`deploy/nginx/`, `docs/DEPLOY.md` §13).
- `obs` / `observer` — observability (Grafana/Loki/Prometheus или OpenObserve + OTel collector). - `obs` / `observer` — observability (Grafana/Loki/Prometheus или OpenObserve + Vector).
Порты на хосте (смещены, чтобы не конфликтовать): Postgres `15432`, Redis `17379`, Порты на хосте (смещены, чтобы не конфликтовать): Postgres `15432`, Redis `17379`,
RabbitMQ AMQP `5672` / UI `15672`, MinIO `9000` / console `9001`, api `8000` / metrics `9100`, RabbitMQ AMQP `5672` / UI `15672`, MinIO `9000` / console `9001`, api `8000` / metrics `9100`
worker metrics: extract `9101`, analyze `9102`, notify `9103`, prescreen `9104`, billing `9105`, edge `80`/`443`. (по умолчанию только на loopback), OpenObserve UI `5080`, edge `80`/`443`.
Метрики worker-ов больше не публикуются на хост; их скрейпит Vector внутри сети compose.
## API (кратко) ## API (кратко)

View file

@ -1,70 +0,0 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Scrape the Prometheus /metrics endpoints exposed by api and workers.
prometheus:
config:
scrape_configs:
- job_name: contract-check
static_configs:
- targets:
- api:9100
- worker-extract:9101
- worker-analyze:9102
- worker-prescreen:9104
- worker-billing:9105
- worker-notify:9103
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
# Forward logs and traces to the OpenObserve OTLP/HTTP endpoint.
# The /openobserve prefix matches ZO_BASE_URI in docker-compose.yml.
otlphttp/openobserve:
endpoint: http://openobserve:5080/openobserve/api/default
headers:
Authorization: Basic ${env:OPENOBSERVE_AUTH_TOKEN}
tls:
insecure: true
# OpenObserve metrics are ingested via Prometheus remote-write.
prometheusremotewrite/openobserve:
endpoint: http://openobserve:5080/openobserve/api/default/prometheus/api/v1/write
headers:
Authorization: Basic ${env:OPENOBSERVE_AUTH_TOKEN}
tls:
insecure: true
# Echo pipeline data to the collector's own logs (useful for debugging).
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/openobserve]
metrics:
receivers: [otlp, prometheus]
processors: [batch]
exporters: [prometheusremotewrite/openobserve]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/openobserve]
# Collector's own telemetry (optional, useful for debugging).
telemetry:
logs:
level: info
metrics:
level: detailed

View file

@ -0,0 +1,115 @@
# Vector configuration for the `observer` profile (passive observability).
#
# Vector reads container logs from the local Docker socket and scrapes service
# /metrics endpoints inside the compose network, then forwards logs and metrics
# to OpenObserve. The application itself does not push telemetry.
# -----------------------------------------------------------------------------
# Sources
# -----------------------------------------------------------------------------
sources:
docker_logs:
type: docker_logs
# Read from the local Docker socket mounted by compose.
docker_host: unix:///var/run/docker.sock
include_labels:
- com.docker.compose.project=dealdocumentscreening
service_metrics:
type: prometheus_scrape
endpoints:
- http://api:8000/metrics
- http://worker-extract:9101/metrics
- http://worker-analyze:9102/metrics
- http://worker-prescreen:9104/metrics
- http://worker-billing:9105/metrics
- http://worker-notify:9103/metrics
scrape_interval_secs: 15
# Auth header is injected via a remap transform when METRICS_BEARER_TOKEN is set.
# -----------------------------------------------------------------------------
# Transforms
# -----------------------------------------------------------------------------
transforms:
enrich_logs:
type: remap
inputs:
- docker_logs
source: |
# Keep the raw message and container name; derive a short service label.
.container = .container_name ?? .container_id ?? "unknown"
.service = .container_name ?? "unknown"
# If the log line is JSON from our structured logger, merge its fields
# without overwriting Vector/container metadata.
raw = .message ?? ""
if is_string(raw) {
parsed, err = parse_json(raw)
if err == null && is_object(parsed) {
for_each(parsed) -> |key, value| {
if !exists(.) || !exists(.[key]) {
.[key] = value
}
}
# Ensure OpenObserve can filter by the canonical service label even when
# the JSON payload carries its own "service" field.
if exists(parsed.service) {
.service = parsed.service
}
if exists(parsed.correlation_id) {
.correlation_id = parsed.correlation_id
}
}
}
# Trim container runtime prefixes for a cleaner service label.
.service = replace(.service, "^contract_check-", "")
add_metrics_auth:
type: remap
inputs:
- service_metrics
source: |
token = get_env_var("METRICS_BEARER_TOKEN") ?? ""
if token != "" {
.headers = {"Authorization": "Bearer " + token}
}
# -----------------------------------------------------------------------------
# Sinks
# -----------------------------------------------------------------------------
sinks:
openobserve_logs:
type: http
inputs:
- enrich_logs
uri: http://openobserve:5080/openobserve/api/default/_json
method: post
auth:
strategy: basic
user: ""
password: ${OPENOBSERVE_AUTH_TOKEN}
encoding:
codec: json
batch:
max_events: 100
timeout_secs: 1
request:
headers:
Content-Type: application/json
openobserve_metrics:
type: prometheus_remote_write
inputs:
- add_metrics_auth
endpoint: http://openobserve:5080/openobserve/api/default/prometheus/api/v1/write
auth:
strategy: basic
user: ""
password: ${OPENOBSERVE_AUTH_TOKEN}
batch:
max_events: 100
timeout_secs: 1

View file

@ -150,10 +150,9 @@ services:
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/} RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
S3_ENDPOINT_URL: http://minio:9000 S3_ENDPOINT_URL: http://minio:9000
OTEL_SERVICE_NAME: api
ports: ports:
- "${API_PORT:-8000}:8000" - "${API_PORT:-8000}:8000"
- "${API_METRICS_PORT:-9100}:9100" - "${API_METRICS_BIND_HOST:-127.0.0.1}:${API_METRICS_PORT:-9100}:9100"
healthcheck: healthcheck:
test: test:
- CMD-SHELL - CMD-SHELL
@ -188,9 +187,6 @@ services:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/} RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
S3_ENDPOINT_URL: http://minio:9000 S3_ENDPOINT_URL: http://minio:9000
OTEL_SERVICE_NAME: worker-extract
ports:
- "9101:9101"
worker-analyze: worker-analyze:
profiles: ["services"] profiles: ["services"]
@ -217,9 +213,6 @@ services:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/} RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
S3_ENDPOINT_URL: http://minio:9000 S3_ENDPOINT_URL: http://minio:9000
OTEL_SERVICE_NAME: worker-analyze
ports:
- "9102:9102"
worker-prescreen: worker-prescreen:
profiles: ["services"] profiles: ["services"]
@ -246,9 +239,6 @@ services:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/} RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
S3_ENDPOINT_URL: http://minio:9000 S3_ENDPOINT_URL: http://minio:9000
OTEL_SERVICE_NAME: worker-prescreen
ports:
- "9104:9104"
worker-billing: worker-billing:
profiles: ["services"] profiles: ["services"]
@ -267,9 +257,6 @@ services:
required: false required: false
environment: environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
OTEL_SERVICE_NAME: worker-billing
ports:
- "9105:9105"
worker-notify: worker-notify:
profiles: ["services"] profiles: ["services"]
@ -292,9 +279,6 @@ services:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/} RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
S3_ENDPOINT_URL: http://minio:9000 S3_ENDPOINT_URL: http://minio:9000
OTEL_SERVICE_NAME: worker-notify
ports:
- "9103:9103"
# Telegram bot adapter (aiogram 3, HTTP-only to api). Per docs/ARCHITECTURE.md §17 # Telegram bot adapter (aiogram 3, HTTP-only to api). Per docs/ARCHITECTURE.md §17
# the bot holds no DB/MQ/S3 credentials — it talks only to the API. # the bot holds no DB/MQ/S3 credentials — it talks only to the API.
@ -399,16 +383,15 @@ services:
# ── OBSERVABILITY ────────────────────────────────────────────────────────── # ── OBSERVABILITY ──────────────────────────────────────────────────────────
# Two observability stacks are provided; activate exactly one profile. # Two observability stacks are provided; activate exactly one profile.
# #
# profile: observer → OpenObserve (lightweight, single binary). # profile: observer → OpenObserve via Vector (lightweight, single binary).
# Use for dev/homelab or when hardware is tight. # Vector scrapes container logs from the Docker socket
# and scrapes service /metrics endpoints; application
# code does not push telemetry.
# profile: obs → Grafana + Prometheus + Loki (mature, heavier). # profile: obs → Grafana + Prometheus + Loki (mature, heavier).
# Use for production-grade visibility. # Use for production-grade visibility.
# #
# App services send OTLP traces/metrics/logs to the endpoint configured in # For the `observer` profile, set OPENOBSERVE_AUTH_TOKEN so Vector can write
# OTEL_EXPORTER_OTLP_ENDPOINT (.env). Point it at otel-collector:4318 for the # to OpenObserve. No OTLP endpoint configuration is needed in the app.
# lightweight profile (the app uses OTLP/HTTP exporters). The collector then
# forwards logs/metrics/traces to OpenObserve. For the obs profile you can
# point this at your own otel-collector/tempo or leave it empty.
# ── LIGHTWEIGHT OBSERVABILITY (profile: observer) ─────────────────────────── # ── LIGHTWEIGHT OBSERVABILITY (profile: observer) ───────────────────────────
openobserve: openobserve:
@ -429,8 +412,7 @@ services:
volumes: volumes:
- openobserve-data:/data - openobserve-data:/data
ports: ports:
- "${OPENOBSERVE_PORT:-5080}:5080" # UI + OTLP/HTTP - "${OPENOBSERVE_PORT:-5080}:5080" # UI + ingestion endpoints
- "${OPENOBSERVE_GRPC_PORT:-5081}:5081" # OTLP/gRPC
deploy: deploy:
resources: resources:
limits: limits:
@ -440,20 +422,17 @@ services:
cpus: "${OPENOBSERVE_CPU_RESERVATION:-0.5}" cpus: "${OPENOBSERVE_CPU_RESERVATION:-0.5}"
memory: "${OPENOBSERVE_MEMORY_RESERVATION:-512M}" memory: "${OPENOBSERVE_MEMORY_RESERVATION:-512M}"
otel-collector: vector:
profiles: ["observer"] profiles: ["observer"]
image: otel/opentelemetry-collector-contrib:latest image: timberio/vector:0.43.0-alpine
container_name: contract_check-otel-collector container_name: contract_check-vector
restart: unless-stopped restart: unless-stopped
command: ["--config", "/etc/otelcol/config.yaml"]
environment: environment:
OPENOBSERVE_AUTH_TOKEN: ${OPENOBSERVE_AUTH_TOKEN} OPENOBSERVE_AUTH_TOKEN: ${OPENOBSERVE_AUTH_TOKEN}
METRICS_BEARER_TOKEN: ${METRICS_BEARER_TOKEN:-}
volumes: volumes:
- ./deploy/observability/otel-collector-config.yaml:/etc/otelcol/config.yaml:ro - /var/run/docker.sock:/var/run/docker.sock:ro
ports: - ./deploy/observability/vector-config.yaml:/etc/vector/vector.yaml:ro
- "${OTEL_COLLECTOR_OTLP_GRPC_PORT:-4317}:4317" # OTLP gRPC receiver
- "${OTEL_COLLECTOR_OTLP_HTTP_PORT:-4318}:4318" # OTLP HTTP receiver
- "${OTEL_COLLECTOR_METRICS_PORT:-8889}:8889" # Collector self-metrics
depends_on: depends_on:
openobserve: openobserve:
condition: service_started condition: service_started

View file

@ -13,7 +13,7 @@
> **workers** (extract/OCR, deterministic prescreen, and LLM-analyze, split for > **workers** (extract/OCR, deterministic prescreen, and LLM-analyze, split for
> CPU vs I/O profiles), a Telegram **bot** adapter, backed by > CPU vs I/O profiles), a Telegram **bot** adapter, backed by
> **Postgres + RabbitMQ + MinIO + Redis**, observable via > **Postgres + RabbitMQ + MinIO + Redis**, observable via
> **structlog + Sentry + Prometheus/Grafana + OpenTelemetry**, behind > **structlog + Sentry + Prometheus/Grafana + OpenObserve/Vector**, behind
> **Nginx + certbot**. Monorepo, six fine-tuned Docker images > **Nginx + certbot**. Monorepo, six fine-tuned Docker images
> (`srv/` — api + three workers + bot + prototype), shared `contract_check.core` > (`srv/` — api + three workers + bot + prototype), shared `contract_check.core`
> domain package. Compose now, k8s-ready later. > domain package. Compose now, k8s-ready later.
@ -51,7 +51,8 @@ Additional decisions locked during the architecture questionnaire:
- **Refund policy is a runtime switch** (`REFUND_POLICY=all|infra_only`) with - **Refund policy is a runtime switch** (`REFUND_POLICY=all|infra_only`) with
a failure-class taxonomy; credit refund stays idempotent. a failure-class taxonomy; credit refund stays idempotent.
- **Observability is full**, not logs-only: structlog+correlation_id, Sentry, - **Observability is full**, not logs-only: structlog+correlation_id, Sentry,
Prometheus+Grafana, OpenTelemetry. Prometheus+Grafana; the lightweight `observer` profile uses Vector+OpenObserve.
OpenTelemetry push has been removed in favour of passive collection.
- **Payments (ЮKassa) live** behind a provider port (`core/billing/port.py`), - **Payments (ЮKassa) live** behind a provider port (`core/billing/port.py`),
webhook-driven state machine, plans/subscriptions + credit top-ups, and a webhook-driven state machine, plans/subscriptions + credit top-ups, and a
`worker-billing` scheduler (renewals/expiry/reconciliation). Disabled by `worker-billing` scheduler (renewals/expiry/reconciliation). Disabled by
@ -129,7 +130,7 @@ Control plane:
| Extra tables | jobs, service_tokens, invoices(stub) | 3-table plan | | Extra tables | jobs, service_tokens, invoices(stub) | 3-table plan |
| Report storage | JSONB + markdown column | — | | Report storage | JSONB + markdown column | — |
| Durability | HA-*ready* (quorum queues, WAL archive) | — | | Durability | HA-*ready* (quorum queues, WAL archive) | — |
| Observability | structlog+corr, Sentry, Prom/Grafana, OTel | docker logs | | Observability | structlog+corr, Sentry, Prom/Grafana, Vector/OpenObserve | docker logs |
| LLM | Provider port + Ollama Cloud adapter | direct client | | LLM | Provider port + Ollama Cloud adapter | direct client |
| Refund | Policy switch `all\|infra_only` + failure classes | refund-all | | Refund | Policy switch `all\|infra_only` + failure classes | refund-all |
| Edge | Nginx + certbot | — | | Edge | Nginx + certbot | — |
@ -167,7 +168,7 @@ DealDocumentScreening/
│ │ └── certbot-init.sh (initial cert + nginx reload) │ │ └── certbot-init.sh (initial cert + nginx reload)
│ └── observability/ │ └── observability/
│ ├── prometheus/prometheus.yml (scrape api + workers :9100..:9105) │ ├── prometheus/prometheus.yml (scrape api + workers :9100..:9105)
│ ├── otel-collector-config.yaml (OpenObserve profile: receiver + forwarder) │ ├── vector-config.yaml (OpenObserve profile: Vector log/metric collection)
│ ├── loki-config.yaml (Grafana/Loki profile) │ ├── loki-config.yaml (Grafana/Loki profile)
│ ├── tempo.yaml (planned — trace storage for Grafana stack) │ ├── tempo.yaml (planned — trace storage for Grafana stack)
│ └── grafana/provisioning/ │ └── grafana/provisioning/
@ -185,7 +186,7 @@ DealDocumentScreening/
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── config.py (pydantic-settings: base + per-service, §11) │ │ ├── config.py (pydantic-settings: base + per-service, §11)
│ │ ├── logging.py (structlog JSON + correlation_id contextvar) │ │ ├── logging.py (structlog JSON + correlation_id contextvar)
│ │ ├── telemetry.py (OTel SDK init, FastAPI/asyncio instrumentation) │ │ ├── telemetry.py (no-op stubs; OTLP push removed)
│ │ ├── sentry.py (sentry_sdk init helper) │ │ ├── sentry.py (sentry_sdk init helper)
│ │ ├── metrics.py (prometheus_client registry + counters/hists) │ │ ├── metrics.py (prometheus_client registry + counters/hists)
│ │ ├── api_keys.py (B2B key gen/hash/verify — sha256 + hmac.compare_digest) │ │ ├── api_keys.py (B2B key gen/hash/verify — sha256 + hmac.compare_digest)
@ -1028,7 +1029,7 @@ change.
### Volumes & restart ### Volumes & restart
- Named volumes for `pgdata`, `rabbitmq`, `minio`, `redis`, `prometheus`, - Named volumes for `pgdata`, `rabbitmq`, `minio`, `redis`, `prometheus`,
`grafana`, `tempo`. Bind-mount only `./deploy` configs and cert dirs. `grafana`. Bind-mount only `./deploy` configs and cert dirs.
- `restart: unless-stopped` on every long-running service. - `restart: unless-stopped` on every long-running service.
- `depends_on: condition: service_healthy` everywhere with real healthchecks - `depends_on: condition: service_healthy` everywhere with real healthchecks
(pg `pg_isready`, rabbit `rabbitmq-diagnostics ping`, minio `mc ready`, (pg `pg_isready`, rabbit `rabbitmq-diagnostics ping`, minio `mc ready`,
@ -1086,12 +1087,10 @@ None of 15 requires touching `core/` application code — only compose/infra.
| `REFUND_WINDOW_DAYS` | `14` | full-refund window (§8a) | | `REFUND_WINDOW_DAYS` | `14` | full-refund window (§8a) |
| `REFUND_FULL_USAGE_THRESHOLD` | `0.20` | usage ratio for full refund | | `REFUND_FULL_USAGE_THRESHOLD` | `0.20` | usage ratio for full refund |
| `SENTRY_DSN` | (empty) | if set, init sentry | | `SENTRY_DSN` | (empty) | if set, init sentry |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | (empty) | OTLP endpoint; with profile `observer` use `http://otel-collector:4318` | | `OPENOBSERVE_AUTH_TOKEN` | default creds base64 | Basic auth token Vector uses to forward logs/metrics to OpenObserve |
| `OTEL_SERVICE_NAME` | per-service | overridden in each service settings | | `OTEL_EXPORTER_OTLP_ENDPOINT` | (removed) | no longer used; the application does not push OTLP |
| `OPENOBSERVE_AUTH_TOKEN` | default creds base64 | Basic auth token the collector uses to forward to OpenObserve | | `OTEL_EXPORTER_OTLP_HEADERS` | (removed) | no longer used |
| `OTEL_COLLECTOR_OTLP_GRPC_PORT` | `4317` | host port for collector OTLP/gRPC receiver | | `OTEL_SERVICE_NAME` | (removed) | service name is set by `configure_logging()` and compose labels |
| `OTEL_COLLECTOR_OTLP_HTTP_PORT` | `4318` | host port for collector OTLP/HTTP receiver |
| `OTEL_COLLECTOR_METRICS_PORT` | `8889` | host port for collector self-metrics |
| `GRAFANA_PORT` | `3000` | host port for the Grafana UI (profile `obs`) | | `GRAFANA_PORT` | `3000` | host port for the Grafana UI (profile `obs`) |
| `GRAFANA_ADMIN_USER` | `admin` | initial Grafana admin user | | `GRAFANA_ADMIN_USER` | `admin` | initial Grafana admin user |
| `GRAFANA_ADMIN_PASSWORD` | `admin` | initial Grafana admin password | | `GRAFANA_ADMIN_PASSWORD` | `admin` | initial Grafana admin password |
@ -1141,9 +1140,10 @@ None of 15 requires touching `core/` application code — only compose/infra.
|---|---|---| |---|---|---|
| `API_HOST` | `0.0.0.0` | | | `API_HOST` | `0.0.0.0` | |
| `API_PORT` | `8000` | | | `API_PORT` | `8000` | |
| `API_METRICS_PORT` | `9100` | | | `API_METRICS_BIND_HOST` | `127.0.0.1` | host interface the API /metrics port is bound to (compose only) |
| `API_METRICS_PORT` | `9100` | host port the API /metrics endpoint is published on (compose only; default loopback) |
| `B2B_DEFAULT_RATE_LIMIT_RPS` | `3` | per API key; mirrors Ollama Pro concurrency, overridable per `api_keys.rate_limit_rps` | | `B2B_DEFAULT_RATE_LIMIT_RPS` | `3` | per API key; mirrors Ollama Pro concurrency, overridable per `api_keys.rate_limit_rps` |
| `METRICS_BEARER_TOKEN` | (empty) | Bearer token protecting `/metrics`; empty leaves the endpoint open | | `METRICS_BEARER_TOKEN` | (empty) | Bearer token protecting `/metrics`; empty leaves the endpoint open, so set a secret in production |
| `CORS_ORIGINS` | (empty, future web) | | | `CORS_ORIGINS` | (empty, future web) | |
### Auth (JWT + Telegram identity verification + webUI + admin panel) ### Auth (JWT + Telegram identity verification + webUI + admin panel)
@ -1249,25 +1249,21 @@ dependencies = [
db = ["sqlalchemy>=2.0", "asyncpg>=0.29", "alembic>=1.13"] db = ["sqlalchemy>=2.0", "asyncpg>=0.29", "alembic>=1.13"]
mq = ["aio-pika>=9.4"] mq = ["aio-pika>=9.4"]
s3 = ["minio>=7.2"] s3 = ["minio>=7.2"]
obs = ["prometheus-client>=0.20", "sentry-sdk>=2", obs = ["prometheus-client>=0.20", "sentry-sdk>=2"]
"opentelemetry-sdk>=1.24", "opentelemetry-exporter-otlp>=1.24"]
api = [{ include-group = "db" }, { include-group = "mq" }, api = [{ include-group = "db" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "obs" }, { include-group = "s3" }, { include-group = "obs" },
"fastapi>=0.110", "uvicorn[standard]>=0.29", "python-multipart>=0.0.9", "fastapi>=0.110", "uvicorn[standard]>=0.29", "python-multipart>=0.0.9",
"redis>=5.0", "redis>=5.0"]
"opentelemetry-instrumentation-fastapi>=0.45b0",
"opentelemetry-instrumentation-asgi>=0.45b0"]
extract = [{ include-group = "db" }, { include-group = "mq" }, extract = [{ include-group = "db" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "obs" }, { include-group = "s3" }, { include-group = "obs" },
"pymupdf>=1.24", "pytesseract>=0.3.10", "pillow>=10", "pymupdf>=1.24", "pytesseract>=0.3.10", "pillow>=10",
"mammoth>=1.8", "striprtf>=0.0.26", "chardet>=5.2", "mammoth>=1.8", "striprtf>=0.0.26", "chardet>=5.2",
"python-magic>=0.4.27"] "python-magic>=0.4.27"]
analyze = [{ include-group = "db" }, { include-group = "mq" }, analyze = [{ include-group = "db" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "obs" }, { include-group = "s3" }, { include-group = "obs" }]
"opentelemetry-instrumentation-httpx>=0.45b0"]
prescreen = [{ include-group = "db" }, { include-group = "mq" }, prescreen = [{ include-group = "db" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "obs" }] { include-group = "s3" }, { include-group = "obs" }]
bot = ["aiogram>=3.4"] bot = ["aiogram>=3.4"]
dev = [{ include-group = "api" }, { include-group = "extract" }, dev = [{ include-group = "api" }, { include-group = "extract" },
{ include-group = "prescreen" }, { include-group = "analyze" }, { include-group = "prescreen" }, { include-group = "analyze" },
@ -1283,8 +1279,8 @@ One file, profiles. Default (`docker compose up`) = infra only.
- `services` profile = api + workers (no bot). - `services` profile = api + workers (no bot).
- `bot` profile = Telegram bot adapter; can run on the same host or a separate - `bot` profile = Telegram bot adapter; can run on the same host or a separate
server (`docs/DEPLOY.md` §14). server (`docs/DEPLOY.md` §14).
- `obs` (Grafana + Loki + Prometheus) and `observer` (OpenObserve + OTel - `obs` (Grafana + Loki + Prometheus) and `observer` (OpenObserve + Vector)
Collector) are mutually exclusive observability stacks. are mutually exclusive observability stacks.
- `edge` = Nginx + certbot. - `edge` = Nginx + certbot.
``` ```
@ -1316,9 +1312,9 @@ services:
prometheus: prom/prometheus; scrapes service /metrics endpoints prometheus: prom/prometheus; scrapes service /metrics endpoints
# ── OBSERVABILITY — OPENOBSERVE STACK (profile: observer) ── # ── OBSERVABILITY — OPENOBSERVE STACK (profile: observer) ──
openobserve: public.ecr.aws/zinclabs/openobserve:latest; UI + OTLP backend on :5080, gRPC on :5081 openobserve: public.ecr.aws/zinclabs/openobserve:latest; UI + JSON/Prometheus-write backend on :5080
otel-collector: otel/opentelemetry-collector-contrib:latest; OTLP receiver 4317/4318, vector: timberio/vector:0.43.0-alpine; reads Docker logs + scrapes /metrics,
scrapes /metrics from services, forwards logs/metrics/traces to OpenObserve forwards logs/metrics to OpenObserve
# ── EDGE (profile: edge) ── # ── EDGE (profile: edge) ──
nginx: nginx:alpine; reverse proxy → api/openobserve/grafana; TLS via certbot nginx: nginx:alpine; reverse proxy → api/openobserve/grafana; TLS via certbot
@ -1344,7 +1340,8 @@ enforcing the adapter boundary even in dependency ordering.
- **workers** read `headers["x-correlation-id"]` on consume and set the same - **workers** read `headers["x-correlation-id"]` on consume and set the same
contextvar before handling. So a single upload's logs trace contextvar before handling. So a single upload's logs trace
api → rabbit → worker-extract → worker-analyze → DB under one ID. api → rabbit → worker-extract → worker-analyze → DB under one ID.
- OTel baggage/span context propagates the same ID for distributed traces. - Distributed tracing is intentionally out of scope; `correlation_id` in
structlog already provides request-level correlation across services.
- The same `correlation_id` is parsed by the Loki datasource as a derived field, - The same `correlation_id` is parsed by the Loki datasource as a derived field,
so you can click through from any log line to every other log line with the so you can click through from any log line to every other log line with the
same id. same id.
@ -1391,35 +1388,34 @@ enforcing the adapter boundary even in dependency ordering.
- `prometheus` in the `obs` profile scrapes `:9100`/`:9101`/`:9102`/`:9104`/`:9105`/`:9103` - `prometheus` in the `obs` profile scrapes `:9100`/`:9101`/`:9102`/`:9104`/`:9105`/`:9103`
metrics exposed by the services. metrics exposed by the services.
- Tempo will receive OTLP traces via the OpenTelemetry collector when added. - Tracing may be added later as a separate feature evaluated against the
passive-collection principle.
- Starter dashboards for queue depth, job latency, LLM tokens/min, refund rate, - Starter dashboards for queue depth, job latency, LLM tokens/min, refund rate,
and 429/fallback rate will ship with the metrics/traces datasources. and 429/fallback rate will ship with the metrics datasources.
### OpenObserve + OTel Collector (`observer` profile) ### OpenObserve + Vector (`observer` profile)
- `otel-collector` runs in the `observer` profile and listens for OTLP on - `vector` runs in the `observer` profile. It reads container logs from the
`4317` (gRPC) and `4318` (HTTP). local Docker socket and scrapes Prometheus `/metrics` endpoints inside the
- Services export OTLP traces, metrics, and logs to `http://otel-collector:4318` compose network (`api:8000/metrics`, workers on `:9101`/`:9102`/`:9104`/`:9105`/`:9103`).
(`OTEL_EXPORTER_OTLP_ENDPOINT`). - Vector enriches logs with `service` (container name) and preserves
- The collector also scrapes Prometheus `/metrics` from `api:9100` and each `correlation_id` from JSON log lines.
worker (`:9101`/`:9102`/`:9104`/`:9105`/`:9103`). - Logs are sent to `http://openobserve:5080/openobserve/api/default/_json` and
- `otlphttp/openobserve` exporter forwards logs and traces to metrics to `http://openobserve:5080/openobserve/api/default/prometheus/api/v1/write`,
`http://openobserve:5080/openobserve/api/default` using `OPENOBSERVE_AUTH_TOKEN` both authenticated with `OPENOBSERVE_AUTH_TOKEN`.
(the `/openobserve` prefix matches `ZO_BASE_URI`).
- `prometheusremotewrite/openobserve` exporter forwards metrics to
`http://openobserve:5080/openobserve/api/default/prometheus/api/v1/write`
because OpenObserve ingests metrics through the Prometheus remote-write path.
- OpenObserve UI is exposed under `/openobserve/` via nginx. - OpenObserve UI is exposed under `/openobserve/` via nginx.
- Worker metrics ports are no longer published on the Docker host; they are only
reachable inside the compose network for scraping.
- The API metrics port is bound to `127.0.0.1` by default to avoid accidental
public exposure.
### OpenTelemetry (`core/telemetry.py`) ### Telemetry module (`core/telemetry.py`)
- `setup_telemetry()` / `shutdown_telemetry()` are safe no-op stubs. The
application does not import or initialize the OpenTelemetry SDK.
- `configure_logging` routes structlog through a root `StreamHandler`, so logs
reach stdout/stderr and are collected passively by Vector or Promtail.
- OTLP exporter sends traces, metrics, and logs to the endpoint configured in
`OTEL_EXPORTER_OTLP_ENDPOINT` (collector in `observer` profile, Tempo in `obs`).
- Auto-instrument FastAPI (api), httpx (all outbound, incl. Ollama calls).
- `configure_logging` routes structlog through stdlib logging so the OTEL log
handler captures application logs with their structured fields.
- Spans carry the same `correlation_id` as logs. Trace from HTTP request →
RabbitMQ publish → consume → LLM call is one trace tree.
--- ---
@ -1690,7 +1686,7 @@ No business logic, no direct state. The hexagonal rule.
- **One image = one entrypoint**; `MODE` dispatcher is dead. - **One image = one entrypoint**; `MODE` dispatcher is dead.
- **Publisher confirms** on; a published-but-unconfirmed message fails the - **Publisher confirms** on; a published-but-unconfirmed message fails the
HTTP request. HTTP request.
- **Correlation ID** flows HTTP → message header → logs → traces. - **Correlation ID** flows HTTP → message header → logs.
--- ---
@ -1757,7 +1753,7 @@ Each step is a verifiable unit. Do not start step N+1 until N is green
- ЮKassa + `invoices` logic + recurring (separate effort). - ЮKassa + `invoices` logic + recurring (separate effort).
- React SPA + Telegram Login + SSE/webhook delivery. - React SPA + Telegram Login + SSE/webhook delivery.
- Grafana dashboards polished; Tempo/Jaeger trace UI. - Grafana dashboards polished.
--- ---

View file

@ -144,9 +144,10 @@ PRICE_PER_DOC_KOPECKS=19900 # 199 ₽ за документ без подпис
# --- Observability (опционально) --- # --- Observability (опционально) ---
SENTRY_DSN=https://...@sentry.io/... SENTRY_DSN=https://...@sentry.io/...
# OTLP/HTTP endpoint. With profile `observer` use the local collector. # OpenObserve auth token used by Vector in the `observer` profile.
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 # Generate with: echo -n 'user:pass' | base64
OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM= OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
# OTEL_EXPORTER_OTLP_ENDPOINT больше не используется: приложение не шлёт OTLP.
``` ```
> Полный список: см. `.env.example` и `ARCHITECTURE.md §11`. Включение > Полный список: см. `.env.example` и `ARCHITECTURE.md §11`. Включение
@ -201,7 +202,7 @@ make shell-db # UPDATE plans SET price_kopecks = 59000 WHERE code = 'lite';
| `services` | `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-billing`, `worker-notify` | | `services` | `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-billing`, `worker-notify` |
| `bot` | `bot` — Telegram-адаптер; можно поднять на этом же хосте или на отдельном сервере | | `bot` | `bot` — Telegram-адаптер; можно поднять на этом же хосте или на отдельном сервере |
| `obs` | `loki`, `promtail`, `grafana`, `prometheus` (logs + metrics; traces — позже) | | `obs` | `loki`, `promtail`, `grafana`, `prometheus` (logs + metrics; traces — позже) |
| `observer` | `openobserve`, `otel-collector` (logs + metrics + traces через collector) | | `observer` | `openobserve`, `vector` (logs + metrics; приложение не шлёт OTLP) |
| `edge` | `nginx`, `certbot` | | `edge` | `nginx`, `certbot` |
```bash ```bash
@ -220,7 +221,7 @@ docker compose --profile services --profile bot up -d --build
# + observability Grafana/Loki/Prometheus (логи + метрики): # + observability Grafana/Loki/Prometheus (логи + метрики):
docker compose --profile services --profile bot --profile obs up -d --build docker compose --profile services --profile bot --profile obs up -d --build
# + observability OpenObserve (логи + метрики + трейсы через otel-collector): # + observability OpenObserve (логи + метрики через Vector; приложение не шлёт OTLP):
docker compose --profile services --profile bot --profile observer up -d --build docker compose --profile services --profile bot --profile observer up -d --build
# + edge (nginx reverse proxy + TLS; deploy/nginx готов): # + edge (nginx reverse proxy + TLS; deploy/nginx готов):
@ -305,7 +306,9 @@ docker compose --profile bot logs -f bot
```bash ```bash
curl http://localhost:8000/healthz # liveness — 200 curl http://localhost:8000/healthz # liveness — 200
curl http://localhost:8000/readyz # readiness — 200 (PG + Rabbit + MinIO) curl http://localhost:8000/readyz # readiness — 200 (PG + Rabbit + MinIO)
curl http://localhost:8000/metrics # Prometheus exposition # /metrics доступен на loopback по умолчанию; снаружи хоста — только через
# edge/nginx или по туннелю. Для проверки внутри контейнера:
docker compose exec api curl -s http://localhost:8000/metrics
``` ```
### 6.2 RabbitMQ Management UI ### 6.2 RabbitMQ Management UI
@ -630,7 +633,8 @@ curl https://contract-check.example.com/healthz
По умолчанию `nginx.conf` проксирует: По умолчанию `nginx.conf` проксирует:
- `/api/v1/*`, `/admin/*` - `/api/v1/*`, `/admin/*`
- `/healthz`, `/readyz` - `/healthz`, `/readyz`
- `/metrics` (открыт наружу; закройте файрволом или уберите приватный скрейпинг) - `/metrics` → проксируется наружу; настройте `METRICS_BEARER_TOKEN` и ограничьте доступ
файрволом/edge, чтобы метрики не утекали наружу. Worker-метрики доступны только внутри сети compose.
- `/webhook/*``/api/v1/webhooks/` - `/webhook/*``/api/v1/webhooks/`
- `${BOT_WEBHOOK_PATH}``bot:8080` (Telegram-бот в webhook-режиме, профиль `bot`; см. §14.4) - `${BOT_WEBHOOK_PATH}``bot:8080` (Telegram-бот в webhook-режиме, профиль `bot`; см. §14.4)
- `/grafana/*``grafana:3000` (когда поднят профиль `obs`) - `/grafana/*``grafana:3000` (когда поднят профиль `obs`)

View file

@ -32,7 +32,7 @@ packages = ["src/contract_check"]
# #
# api → core + db/mq/s3/obs + fastapi/uvicorn # api → core + db/mq/s3/obs + fastapi/uvicorn
# worker-extract → core + db/mq/s3/obs + pymupdf/tesseract # worker-extract → core + db/mq/s3/obs + pymupdf/tesseract
# worker-analyze → core + db/mq/s3/obs + otel-httpx # worker-analyze → core + db/mq/s3/obs
# bot → core + aiogram (leanest image: no db/mq/s3/llm/tesseract) # bot → core + aiogram (leanest image: no db/mq/s3/llm/tesseract)
# dev → everything needed to lint/typecheck/test locally # dev → everything needed to lint/typecheck/test locally
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -51,8 +51,6 @@ s3 = [
obs = [ obs = [
"prometheus-client>=0.20", "prometheus-client>=0.20",
"sentry-sdk>=2", "sentry-sdk>=2",
"opentelemetry-sdk>=1.24",
"opentelemetry-exporter-otlp>=1.24",
] ]
api = [ api = [
{ include-group = "db" }, { include-group = "db" },
@ -68,9 +66,6 @@ api = [
"argon2-cffi>=23.1", "argon2-cffi>=23.1",
"email-validator>=2.1", "email-validator>=2.1",
"webauthn>=2.5", "webauthn>=2.5",
"opentelemetry-instrumentation-fastapi>=0.45b0",
"opentelemetry-instrumentation-asgi>=0.45b0",
"opentelemetry-instrumentation-httpx>=0.45b0",
] ]
extract = [ extract = [
{ include-group = "db" }, { include-group = "db" },
@ -84,21 +79,18 @@ extract = [
"striprtf>=0.0.26", "striprtf>=0.0.26",
"chardet>=5.2", "chardet>=5.2",
"python-magic>=0.4.27", "python-magic>=0.4.27",
"opentelemetry-instrumentation-httpx>=0.45b0",
] ]
analyze = [ analyze = [
{ include-group = "db" }, { include-group = "db" },
{ include-group = "mq" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "s3" },
{ include-group = "obs" }, { include-group = "obs" },
"opentelemetry-instrumentation-httpx>=0.45b0",
] ]
prescreen = [ prescreen = [
{ include-group = "db" }, { include-group = "db" },
{ include-group = "mq" }, { include-group = "mq" },
{ include-group = "s3" }, { include-group = "s3" },
{ include-group = "obs" }, { include-group = "obs" },
"opentelemetry-instrumentation-httpx>=0.45b0",
] ]
bot = [ bot = [
"aiogram>=3.4", "aiogram>=3.4",
@ -112,12 +104,10 @@ notify = [
{ include-group = "mq" }, { include-group = "mq" },
{ include-group = "obs" }, { include-group = "obs" },
"aiosmtplib>=3.0", "aiosmtplib>=3.0",
"opentelemetry-instrumentation-httpx>=0.45b0",
] ]
billing = [ billing = [
{ include-group = "db" }, { include-group = "db" },
{ include-group = "obs" }, { include-group = "obs" },
"opentelemetry-instrumentation-httpx>=0.45b0",
"pyjwt>=2.8", "pyjwt>=2.8",
] ]
dev = [ dev = [

View file

@ -41,7 +41,6 @@ from src.contract_check.core.rate_limit import MemoryRateLimiter, RateLimiter, R
from src.contract_check.core.redis_client import get_redis_client from src.contract_check.core.redis_client import get_redis_client
from src.contract_check.core.s3.minio_storage import MinioStorage from src.contract_check.core.s3.minio_storage import MinioStorage
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
@asynccontextmanager @asynccontextmanager
@ -55,7 +54,6 @@ async def lifespan(app: FastAPI) -> Any:
) )
bind_context(service="api", env=settings.env) bind_context(service="api", env=settings.env)
init_sentry("api") init_sentry("api")
setup_telemetry("api")
storage = MinioStorage.from_endpoint_url( storage = MinioStorage.from_endpoint_url(
endpoint_url=settings.s3_endpoint_url, endpoint_url=settings.s3_endpoint_url,
@ -112,7 +110,6 @@ async def lifespan(app: FastAPI) -> Any:
await redis_client.aclose() await redis_client.aclose()
except Exception: except Exception:
pass pass
shutdown_telemetry()
def create_app() -> FastAPI: def create_app() -> FastAPI:
@ -147,15 +144,6 @@ def create_app() -> FastAPI:
) -> RedirectResponse: ) -> RedirectResponse:
return RedirectResponse(url=f"/admin/login?reason={exc.reason}", status_code=303) return RedirectResponse(url=f"/admin/login?reason={exc.reason}", status_code=303)
# Emit request traces to the OTLP collector. Must happen after all routes
# and middleware are registered so spans cover the full request lifecycle.
try:
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
FastAPIInstrumentor.instrument_app(app)
except Exception: # pragma: no cover - optional instrumentor
pass
return app return app

View file

@ -72,7 +72,8 @@ Readiness-проба. Выполняет `SELECT 1` в БД.
## metrics ## metrics
Файл: [`metrics.py`](metrics.py). Тег: `metrics`. Без аутентификации. Файл: [`metrics.py`](metrics.py). Тег: `metrics`. Без аутентификации, если
`METRICS_BEARER_TOKEN` не задан; иначе требуется `Authorization: Bearer <token>`.
### `GET /metrics` ### `GET /metrics`

View file

@ -7,7 +7,7 @@ Adapters (bot) deliberately do NOT import this package beyond config/logging
Subpackages: Subpackages:
config typed env config (pydantic-settings) config typed env config (pydantic-settings)
logging structlog JSON + correlation_id propagation logging structlog JSON + correlation_id propagation
telemetry OpenTelemetry init telemetry no-op stubs; OTLP push removed
sentry Sentry init sentry Sentry init
metrics Prometheus counters/histograms + metrics HTTP server metrics Prometheus counters/histograms + metrics HTTP server
db SQLAlchemy 2 models (6 tables), async session, enums db SQLAlchemy 2 models (6 tables), async session, enums

View file

@ -99,7 +99,6 @@ def _jwt_refresh_ttl() -> dt.timedelta:
def create_access_token(user_id: uuid.UUID, telegram_id: int) -> str: def create_access_token(user_id: uuid.UUID, telegram_id: int) -> str:
"""Sign a fresh access JWT for a verified user.""" """Sign a fresh access JWT for a verified user."""
settings = get_settings()
now = dt.datetime.now(tz=dt.UTC) now = dt.datetime.now(tz=dt.UTC)
claims = AccessTokenClaims( claims = AccessTokenClaims(
sub=user_id, sub=user_id,
@ -111,7 +110,7 @@ def create_access_token(user_id: uuid.UUID, telegram_id: int) -> str:
{ {
"iat": int(now.timestamp()), "iat": int(now.timestamp()),
"exp": int((now + _jwt_access_ttl()).timestamp()), "exp": int((now + _jwt_access_ttl()).timestamp()),
"iss": settings.otel_service_name or "contract-check", "iss": "contract-check",
"aud": "contract-check", "aud": "contract-check",
} }
) )
@ -182,7 +181,6 @@ class RefreshTokenClaims:
def create_refresh_token(user_id: uuid.UUID, jti: str) -> str: def create_refresh_token(user_id: uuid.UUID, jti: str) -> str:
"""Sign a refresh JWT. `jti` is the lookup key in the refresh-token store.""" """Sign a refresh JWT. `jti` is the lookup key in the refresh-token store."""
settings = get_settings()
now = dt.datetime.now(tz=dt.UTC) now = dt.datetime.now(tz=dt.UTC)
claims = RefreshTokenClaims(sub=user_id, jti=jti, type=JWT_TYPE_REFRESH) claims = RefreshTokenClaims(sub=user_id, jti=jti, type=JWT_TYPE_REFRESH)
payload = claims.to_dict() payload = claims.to_dict()
@ -190,7 +188,7 @@ def create_refresh_token(user_id: uuid.UUID, jti: str) -> str:
{ {
"iat": int(now.timestamp()), "iat": int(now.timestamp()),
"exp": int((now + _jwt_refresh_ttl()).timestamp()), "exp": int((now + _jwt_refresh_ttl()).timestamp()),
"iss": settings.otel_service_name or "contract-check", "iss": "contract-check",
"aud": "contract-check", "aud": "contract-check",
} }
) )

View file

@ -86,8 +86,6 @@ class Settings(BaseSettings):
# --- observability (empty disables) --- # --- observability (empty disables) ---
sentry_dsn: str = "" sentry_dsn: str = ""
otel_exporter_otlp_endpoint: str = ""
otel_service_name: str = "contract-check"
# --- LLM provider --- # --- LLM provider ---
llm_provider: str = "ollama_cloud" llm_provider: str = "ollama_cloud"

View file

@ -183,8 +183,8 @@ def configure_logging(
logger_factory=structlog.stdlib.LoggerFactory(), logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True, cache_logger_on_first_use=True,
) )
# Route stdlib logging through structlog so third-party libs match our format, # Route stdlib logging through structlog so third-party libs match our format.
# and structlog events reach any OTEL handlers attached to the root logger. # Structlog events always reach the root StreamHandler configured below.
stdlib_handler = logging.StreamHandler(sys.stderr) stdlib_handler = logging.StreamHandler(sys.stderr)
stdlib_handler.setFormatter( stdlib_handler.setFormatter(
structlog.stdlib.ProcessorFormatter( structlog.stdlib.ProcessorFormatter(

View file

@ -28,4 +28,4 @@ def init_sentry(service_name: str | None = None) -> None:
traces_sample_rate=traces_sample_rate, traces_sample_rate=traces_sample_rate,
send_default_pii=False, send_default_pii=False,
) )
log.info("sentry_initialized", service=service_name or settings.otel_service_name) log.info("sentry_initialized", service=service_name or "contract-check")

View file

@ -1,167 +1,117 @@
"""OpenTelemetry initialization. """Telemetry stubs after passive-collection refactor.
Sets up OTLP exporters for traces, logs, and metrics when The application no longer pushes OTLP traces, logs, or metrics. Logs are
`OTEL_EXPORTER_OTLP_ENDPOINT` is set; otherwise no-op. httpx auto-instrumentation written to stdout/stderr by ``core/logging.py`` and collected by the
is wired so Ollama Cloud calls appear as spans. Service entrypoints call container runtime or a sidecar (Vector in the ``observer`` profile,
`setup_telemetry()` early and `shutdown_telemetry()` on exit. Promtail in the ``obs`` profile). Prometheus metrics stay in-process and
are scraped from service ``/metrics`` endpoints.
Auth headers are taken from `OTEL_EXPORTER_OTLP_HEADERS` by the OTLP exporters ``setup_telemetry()`` and ``shutdown_telemetry()`` remain as safe no-op
(no code-level parsing needed). entrypoints so service ``__main__`` modules do not all have to change at
once. ``get_tracer()`` and ``get_meter()`` return lightweight no-op objects
This module imports `opentelemetry` only services in the `obs` group import it so any remaining instrumentation calls do not raise.
(see docs/ARCHITECTURE.md §4). mypy resolves it via the dev group.
""" """
from __future__ import annotations from __future__ import annotations
import logging from typing import Any
from typing import TYPE_CHECKING, Any
from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import get_logger
if TYPE_CHECKING:
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
log = get_logger(__name__)
_trace_provider: TracerProvider | None = None
_log_provider: LoggerProvider | None = None
_meter_provider: MeterProvider | None = None
def setup_telemetry(service_name: str | None = None) -> None: class _NoopSpan:
"""Initialize OTel tracing/logging/metrics. Safe to call when disabled.""" """Span-like object that ignores all operations."""
global _trace_provider, _log_provider, _meter_provider
settings = get_settings()
endpoint = settings.otel_exporter_otlp_endpoint
if not endpoint:
log.debug("otel_disabled", reason="no endpoint configured")
return
from opentelemetry._logs import set_logger_provider def end(self, _timestamp: Any = None) -> None:
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter return None
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.metrics import set_meter_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import set_tracer_provider
class StructlogOTLPLogHandler(LoggingHandler): def set_attribute(self, _key: str, _value: Any) -> None:
"""OTEL handler that unwraps structlog event dicts. return None
``core/logging.py`` routes structlog through stdlib logging; the def record_exception(self, _exception: BaseException, _attributes: Any = None) -> None:
resulting ``LogRecord`` carries the event dictionary in return None
``record.msg``. This handler extracts the ``event`` as the log body and
promotes the remaining fields to OTEL attributes so OpenObserve can
index them.
"""
_STRUCTLOG_INTERNAL_KEYS = frozenset({"_logger", "_name"}) def __enter__(self) -> _NoopSpan:
return self
def _translate(self, record: logging.LogRecord) -> Any: def __exit__(self, *args: object) -> None:
# Detect structlog-wrapped records produced by return None
# ``ProcessorFormatter.wrap_for_formatter``.
if (
getattr(record, "_logger", None) is not None
and getattr(record, "_name", None) is not None
and isinstance(record.msg, (tuple, list))
and len(record.msg) == 1
and isinstance(record.msg[0], dict)
):
event_dict = record.msg[0]
# Work on a copy so the original record (used by stderr handler)
# is not mutated.
patched = logging.makeLogRecord(record.__dict__)
patched.msg = event_dict.get("event", "")
patched.args = ()
# Drop structlog internal bookkeeping copied from the original
# record so it does not leak into OTEL attributes.
for key in self._STRUCTLOG_INTERNAL_KEYS:
patched.__dict__.pop(key, None)
for key, value in event_dict.items():
if key in self._STRUCTLOG_INTERNAL_KEYS:
continue
if key not in patched.__dict__:
setattr(patched, key, value)
return super()._translate(patched)
return super()._translate(record)
resource = Resource.create({"service.name": service_name or settings.otel_service_name})
# Traces. Exporters read OTEL_EXPORTER_OTLP_ENDPOINT and append the class _NoopTracer:
# signal-specific path (/v1/traces, /v1/logs, /v1/metrics) automatically. """Tracer-like object that returns no-op spans."""
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
set_tracer_provider(trace_provider)
_trace_provider = trace_provider
# Logs (stdlib logging → OTLP). ``configure_logging`` routes structlog def start_span(self, _name: str, _context: Any = None, _kind: Any = None) -> _NoopSpan:
# through the stdlib logging tree; this handler unwraps the structured return _NoopSpan()
# event dicts so OpenObserve receives the message body and fields.
log_provider = LoggerProvider(resource=resource)
log_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
set_logger_provider(log_provider)
_log_provider = log_provider
otel_log_handler = StructlogOTLPLogHandler(logger_provider=log_provider) def start_as_current_span(self, _name: str, *_args: Any, **_kwargs: Any) -> _NoopSpan:
otel_log_handler.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO)) return _NoopSpan()
logging.getLogger().addHandler(otel_log_handler)
# Metrics infrastructure (OTel metrics → OTLP). Existing prometheus_client
# metrics are still scraped by Prometheus; use remote_write for those.
metric_reader = PeriodicExportingMetricReader(OTLPMetricExporter())
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
set_meter_provider(meter_provider)
_meter_provider = meter_provider
try: class _NoopCounter:
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor """Counter-like object that ignores add() calls."""
HTTPXClientInstrumentor().instrument() def add(self, _amount: int | float, _attributes: Any = None) -> None:
except Exception as exc: # pragma: no cover - optional instrumentor return None
log.warning("otel_httpx_instrument_failed", error=str(exc))
log.info("otel_initialized", endpoint=endpoint)
# Stdlib log so the OTel handler exports a verification record too. class _NoopHistogram:
logging.getLogger("contract_check.telemetry").info( """Histogram-like object that ignores record() calls."""
"OpenTelemetry initialized for %s at %s",
service_name or settings.otel_service_name, def record(self, _amount: int | float, _attributes: Any = None) -> None:
endpoint, return None
)
class _NoopObservableGauge:
"""Observable gauge-like object that ignores callbacks."""
def __init__(self) -> None:
pass
class _NoopMeter:
"""Meter-like object that returns no-op instruments."""
def create_counter(self, _name: str, *_args: Any, **_kwargs: Any) -> _NoopCounter:
return _NoopCounter()
def create_histogram(self, _name: str, *_args: Any, **_kwargs: Any) -> _NoopHistogram:
return _NoopHistogram()
def create_up_down_counter(self, _name: str, *_args: Any, **_kwargs: Any) -> _NoopCounter:
return _NoopCounter()
def create_observable_gauge(
self, _name: str, *_args: Any, **_kwargs: Any
) -> _NoopObservableGauge:
return _NoopObservableGauge()
_TRACER = _NoopTracer()
_METER = _NoopMeter()
def setup_telemetry(_service_name: str | None = None) -> None:
"""No-op telemetry initialization retained for backward compatibility."""
return None
def shutdown_telemetry() -> None: def shutdown_telemetry() -> None:
"""Flush and shut down all OTel providers if initialized.""" """No-op telemetry shutdown retained for backward compatibility."""
global _trace_provider, _log_provider, _meter_provider return None
for provider in (_trace_provider, _log_provider, _meter_provider):
if provider is not None:
try:
provider.shutdown()
except Exception as exc: # pragma: no cover - shutdown must not raise
log.warning("otel_shutdown_failed", error=str(exc))
_trace_provider = None
_log_provider = None
_meter_provider = None
def get_tracer(name: str | None = None) -> Any: def get_tracer(_name: str | None = None) -> _NoopTracer:
"""Return a tracer. Returns a no-op-safe tracer when OTel is absent.""" """Return a no-op tracer.
from opentelemetry import trace
return trace.get_tracer(name or "contract_check") The application no longer uses OpenTelemetry; any existing calls to
``start_span`` or ``start_as_current_span`` are silently ignored.
"""
return _TRACER
def get_meter(name: str | None = None) -> Any: def get_meter(_name: str | None = None) -> _NoopMeter:
"""Return a meter. Returns a no-op-safe meter when OTel is absent.""" """Return a no-op meter.
from opentelemetry import metrics
return metrics.get_meter(name or "contract_check") The application no longer uses OpenTelemetry; any existing instrument
calls are silently ignored.
"""
return _METER

View file

@ -9,7 +9,6 @@ from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import bind_context, configure_logging, get_logger from src.contract_check.core.logging import bind_context, configure_logging, get_logger
from src.contract_check.core.metrics import start_metrics_server from src.contract_check.core.metrics import start_metrics_server
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
from src.contract_check.worker_analyze.consumer import AnalyzeConsumer from src.contract_check.worker_analyze.consumer import AnalyzeConsumer
log = get_logger(__name__) log = get_logger(__name__)
@ -25,7 +24,6 @@ async def main() -> None:
) )
bind_context(service="worker-analyze", env=settings.env) bind_context(service="worker-analyze", env=settings.env)
init_sentry("worker-analyze") init_sentry("worker-analyze")
setup_telemetry("worker-analyze")
start_metrics_server(9102) start_metrics_server(9102)
@ -55,7 +53,6 @@ async def main() -> None:
) )
finally: finally:
await consumer.stop() await consumer.stop()
shutdown_telemetry()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -12,7 +12,6 @@ from src.contract_check.core.db.session import create_session_factory, dispose_e
from src.contract_check.core.logging import bind_context, configure_logging, get_logger from src.contract_check.core.logging import bind_context, configure_logging, get_logger
from src.contract_check.core.metrics import start_metrics_server from src.contract_check.core.metrics import start_metrics_server
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
from src.contract_check.worker_billing.scheduler import run_tick from src.contract_check.worker_billing.scheduler import run_tick
log = get_logger(__name__) log = get_logger(__name__)
@ -30,7 +29,6 @@ async def main() -> None:
) )
bind_context(service="worker-billing", env=settings.env) bind_context(service="worker-billing", env=settings.env)
init_sentry("worker-billing") init_sentry("worker-billing")
setup_telemetry("worker-billing")
start_metrics_server(9105) start_metrics_server(9105)
@ -64,7 +62,6 @@ async def main() -> None:
if provider is not None: if provider is not None:
await provider.aclose() await provider.aclose()
await dispose_engine() await dispose_engine()
shutdown_telemetry()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -9,7 +9,6 @@ from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import bind_context, configure_logging, get_logger from src.contract_check.core.logging import bind_context, configure_logging, get_logger
from src.contract_check.core.metrics import start_metrics_server from src.contract_check.core.metrics import start_metrics_server
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
from src.contract_check.worker_extract.consumer import ExtractConsumer from src.contract_check.worker_extract.consumer import ExtractConsumer
log = get_logger(__name__) log = get_logger(__name__)
@ -25,7 +24,6 @@ async def main() -> None:
) )
bind_context(service="worker-extract", env=settings.env) bind_context(service="worker-extract", env=settings.env)
init_sentry("worker-extract") init_sentry("worker-extract")
setup_telemetry("worker-extract")
start_metrics_server(9101) start_metrics_server(9101)
@ -55,7 +53,6 @@ async def main() -> None:
) )
finally: finally:
await consumer.stop() await consumer.stop()
shutdown_telemetry()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -9,7 +9,6 @@ from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import bind_context, configure_logging, get_logger from src.contract_check.core.logging import bind_context, configure_logging, get_logger
from src.contract_check.core.metrics import start_metrics_server from src.contract_check.core.metrics import start_metrics_server
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
from src.contract_check.worker_notify.consumer import NotifyConsumer from src.contract_check.worker_notify.consumer import NotifyConsumer
log = get_logger(__name__) log = get_logger(__name__)
@ -25,7 +24,6 @@ async def main() -> None:
) )
bind_context(service="worker-notify", env=settings.env) bind_context(service="worker-notify", env=settings.env)
init_sentry("worker-notify") init_sentry("worker-notify")
setup_telemetry("worker-notify")
start_metrics_server(9103) start_metrics_server(9103)
@ -55,7 +53,6 @@ async def main() -> None:
) )
finally: finally:
await consumer.stop() await consumer.stop()
shutdown_telemetry()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -9,7 +9,6 @@ from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import bind_context, configure_logging, get_logger from src.contract_check.core.logging import bind_context, configure_logging, get_logger
from src.contract_check.core.metrics import start_metrics_server from src.contract_check.core.metrics import start_metrics_server
from src.contract_check.core.sentry import init_sentry from src.contract_check.core.sentry import init_sentry
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
from src.contract_check.worker_prescreen.consumer import PrescreenConsumer from src.contract_check.worker_prescreen.consumer import PrescreenConsumer
log = get_logger(__name__) log = get_logger(__name__)
@ -25,7 +24,6 @@ async def main() -> None:
) )
bind_context(service="worker-prescreen", env=settings.env) bind_context(service="worker-prescreen", env=settings.env)
init_sentry("worker-prescreen") init_sentry("worker-prescreen")
setup_telemetry("worker-prescreen")
start_metrics_server(9104) start_metrics_server(9104)
@ -55,7 +53,6 @@ async def main() -> None:
) )
finally: finally:
await consumer.stop() await consumer.stop()
shutdown_telemetry()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -13,8 +13,8 @@ WORKDIR /app
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
COPY README.md ./ COPY README.md ./
# Install only the analyze group (core + db/mq/s3/obs + otel-httpx). No # Install only the analyze group (core + db/mq/s3/obs). No tesseract/pymupdf
# tesseract/pymupdf — this image is the leanest LLM/I/O worker. # — this image is the leanest LLM/I/O worker.
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group analyze --no-install-project uv sync --frozen --no-default-groups --group analyze --no-install-project

View file

@ -1,7 +1,8 @@
"""Unit tests for OpenTelemetry integration. """Unit tests for telemetry after the passive-collection refactor.
Covers the structlog-aware OTEL log handler that unwraps event dicts so The application no longer pushes OTLP. This module verifies that telemetry
OpenObserve receives structured attributes. setup/shutdown are safe no-ops and that structured logging still reaches the
configured root handler.
""" """
from __future__ import annotations from __future__ import annotations
@ -11,6 +12,7 @@ import logging
import pytest import pytest
from contract_check.core.logging import configure_logging, get_logger from contract_check.core.logging import configure_logging, get_logger
from contract_check.core.telemetry import get_meter, get_tracer, setup_telemetry, shutdown_telemetry
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -22,82 +24,64 @@ def _reset_logging():
root.handlers.clear() root.handlers.clear()
def test_structlog_otlp_handler_unwraps_event_dict(monkeypatch: pytest.MonkeyPatch) -> None: def test_setup_telemetry_is_safe_noop() -> None:
"""A structlog-wrapped record is converted to body + attributes.""" """setup_telemetry() runs without error and does not add an OTLP handler."""
from contract_check.core.config import get_settings
from contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
monkeypatch.setattr(
get_settings(), "otel_exporter_otlp_endpoint", "http://localhost:9999/v1/logs"
)
# setup_telemetry defines the handler class when OTEL is imported.
setup_telemetry("test")
root = logging.getLogger() root = logging.getLogger()
before = list(root.handlers)
setup_telemetry("test")
try: try:
otel_handler = next( after = list(root.handlers)
h for h in root.handlers if type(h).__name__ == "StructlogOTLPLogHandler" # No new handlers are attached because OTLP is no longer initialized.
) assert after == before
record = logging.LogRecord(
name="test.logger",
level=logging.INFO,
pathname="test.py",
lineno=5,
msg=({"event": "hello_openobserve", "foo": "bar", "count": 42},),
args=(),
exc_info=None,
)
record._logger = logging.getLogger("test.logger") # type: ignore[attr-defined]
record._name = "info" # type: ignore[attr-defined]
otel_record = otel_handler._translate(record)
assert otel_record.body == "hello_openobserve"
attrs = {k: v for k, v in otel_record.attributes.items() if not k.startswith("code.")}
assert attrs["foo"] == "bar"
assert attrs["count"] == 42
assert "_logger" not in attrs
assert "_name" not in attrs
finally: finally:
shutdown_telemetry() shutdown_telemetry()
root.handlers = [h for h in root.handlers if type(h).__name__ != "StructlogOTLPLogHandler"]
def test_structlog_logs_reach_root_handlers() -> None: def test_shutdown_telemetry_is_safe_noop() -> None:
"""After configure_logging, structlog events propagate to stdlib handlers.""" """shutdown_telemetry() can be called even when setup_telemetry() was skipped."""
shutdown_telemetry()
shutdown_telemetry() # idempotent
def test_otlp_endpoint_env_does_not_initialize_exporters(monkeypatch: pytest.MonkeyPatch) -> None:
"""A legacy OTLP endpoint env var must not cause the application to import OTel SDK."""
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")
from contract_check.core.config import get_settings
# Settings explicitly ignores unknown env vars, but a stale env var must not
# be picked up by telemetry code. Refresh the cached settings instance.
get_settings.cache_clear()
root = logging.getLogger() root = logging.getLogger()
assert ( before = list(root.handlers)
any(type(h).__name__ in ("StreamHandler", "ProcessorFormatter") for h in root.handlers) setup_telemetry("test")
or root.handlers try:
) after = list(root.handlers)
assert after == before
assert not any(type(h).__name__.startswith(("OTLP", "StructlogOTLP")) for h in after)
finally:
shutdown_telemetry()
get_settings.cache_clear()
def test_get_tracer_and_meter_are_safe_noop() -> None:
"""Tracer/meter accessors return objects that do not require OTel SDK."""
tracer = get_tracer("test")
meter = get_meter("test")
# They should be truthy and accept the expected creation calls without raising.
assert tracer
assert meter
span = tracer.start_span("ignored")
span.end()
counter = meter.create_counter("ignored")
counter.add(1)
def test_configure_logging_delivers_events_to_root_stream_handler() -> None:
"""After configure_logging, structlog events propagate to the root StreamHandler."""
root = logging.getLogger()
assert any(isinstance(h, logging.StreamHandler) for h in root.handlers)
log = get_logger("test") log = get_logger("test")
log.info("smoke_test", answer=42) log.info("smoke_test", answer=42)
@pytest.mark.parametrize("level", ["DEBUG", "INFO", "WARNING"])
def test_otel_handler_respects_configured_log_level(
monkeypatch: pytest.MonkeyPatch, level: str
) -> None:
"""The OTLP handler level matches settings.log_level so DEBUG logs can reach OpenObserve."""
from contract_check.core.config import get_settings
from contract_check.core.logging import configure_logging
from contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
configure_logging(level, json_output=True, service="test-service", env="test")
monkeypatch.setattr(
get_settings(), "otel_exporter_otlp_endpoint", "http://localhost:9999/v1/logs"
)
monkeypatch.setattr(get_settings(), "log_level", level)
setup_telemetry("test")
root = logging.getLogger()
try:
otel_handler = next(
h for h in root.handlers if type(h).__name__ == "StructlogOTLPLogHandler"
)
assert otel_handler.level == getattr(logging, level)
finally:
shutdown_telemetry()
root.handlers = [h for h in root.handlers if type(h).__name__ != "StructlogOTLPLogHandler"]

457
uv.lock generated
View file

@ -262,15 +262,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" }, { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" },
] ]
[[package]]
name = "asgiref"
version = "3.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" },
]
[[package]] [[package]]
name = "asyncpg" name = "asyncpg"
version = "0.31.0" version = "0.31.0"
@ -434,100 +425,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/6e/5a0b348fa4cd7847567a28c6e697ccf58391960bfd13a6e7473ee23ca2f2/chardet-7.6.0-py3-none-any.whl", hash = "sha256:4076d795897ce45239825956a1334e134322ecc4bfe84dbb12acd5390de0fbc1", size = 680279, upload-time = "2026-08-14T20:36:57.763Z" }, { url = "https://files.pythonhosted.org/packages/cf/6e/5a0b348fa4cd7847567a28c6e697ccf58391960bfd13a6e7473ee23ca2f2/chardet-7.6.0-py3-none-any.whl", hash = "sha256:4076d795897ce45239825956a1334e134322ecc4bfe84dbb12acd5390de0fbc1", size = 680279, upload-time = "2026-08-14T20:36:57.763Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" },
{ url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" },
{ url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" },
{ url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" },
{ url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" },
{ url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" },
{ url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" },
{ url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" },
{ url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" },
{ url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" },
{ url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" },
{ url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" },
{ url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" },
{ url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" },
{ url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" },
{ url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" },
{ url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" },
{ url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" },
{ url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" },
{ url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" },
{ url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" },
{ url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" },
{ url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" },
{ url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" },
{ url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" },
{ url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" },
{ url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" },
{ url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" },
{ url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" },
{ url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" },
{ url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" },
{ url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" },
{ url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" },
{ url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" },
{ url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" },
{ url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" },
{ url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" },
{ url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" },
{ url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" },
{ url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" },
{ url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" },
{ url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" },
{ url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" },
{ url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" },
{ url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" },
{ url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" },
{ url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" },
{ url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" },
{ url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" },
{ url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" },
{ url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" },
{ url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" },
{ url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" },
{ url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" },
{ url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" },
{ url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" },
{ url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" },
{ url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" },
{ url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" },
{ url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" },
{ url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" },
{ url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" },
{ url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" },
{ url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" },
{ url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" },
{ url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" },
{ url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" },
{ url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.5.0" version = "8.5.0"
@ -573,9 +470,6 @@ analyze = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "minio" }, { name = "minio" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
@ -589,11 +483,6 @@ api = [
{ name = "fastapi" }, { name = "fastapi" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "minio" }, { name = "minio" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "pyjwt", extra = ["crypto"] }, { name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" }, { name = "python-multipart" },
@ -606,9 +495,6 @@ api = [
billing = [ billing = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
@ -642,11 +528,6 @@ dev = [
{ name = "jinja2" }, { name = "jinja2" },
{ name = "mammoth" }, { name = "mammoth" },
{ name = "minio" }, { name = "minio" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "pillow" }, { name = "pillow" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
@ -676,9 +557,6 @@ extract = [
{ name = "chardet" }, { name = "chardet" },
{ name = "mammoth" }, { name = "mammoth" },
{ name = "minio" }, { name = "minio" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "pillow" }, { name = "pillow" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "pymupdf" }, { name = "pymupdf" },
@ -696,16 +574,11 @@ notify = [
{ name = "aiosmtplib" }, { name = "aiosmtplib" },
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
] ]
obs = [ obs = [
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
] ]
@ -714,9 +587,6 @@ prescreen = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "minio" }, { name = "minio" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" }, { name = "prometheus-client" },
{ name = "sentry-sdk" }, { name = "sentry-sdk" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
@ -740,9 +610,6 @@ analyze = [
{ name = "alembic", specifier = ">=1.13" }, { name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "minio", specifier = ">=7.2" }, { name = "minio", specifier = ">=7.2" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "sentry-sdk", specifier = ">=2" }, { name = "sentry-sdk", specifier = ">=2" },
{ name = "sqlalchemy", specifier = ">=2.0" }, { name = "sqlalchemy", specifier = ">=2.0" },
@ -756,11 +623,6 @@ api = [
{ name = "fastapi", specifier = ">=0.110" }, { name = "fastapi", specifier = ">=0.110" },
{ name = "jinja2", specifier = ">=3.1" }, { name = "jinja2", specifier = ">=3.1" },
{ name = "minio", specifier = ">=7.2" }, { name = "minio", specifier = ">=7.2" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-asgi", specifier = ">=0.45b0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.45b0" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.8" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.8" },
{ name = "python-multipart", specifier = ">=0.0.9" }, { name = "python-multipart", specifier = ">=0.0.9" },
@ -773,9 +635,6 @@ api = [
billing = [ billing = [
{ name = "alembic", specifier = ">=1.13" }, { name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "pyjwt", specifier = ">=2.8" }, { name = "pyjwt", specifier = ">=2.8" },
{ name = "sentry-sdk", specifier = ">=2" }, { name = "sentry-sdk", specifier = ">=2" },
@ -809,11 +668,6 @@ dev = [
{ name = "jinja2", specifier = ">=3.1" }, { name = "jinja2", specifier = ">=3.1" },
{ name = "mammoth", specifier = ">=1.8" }, { name = "mammoth", specifier = ">=1.8" },
{ name = "minio", specifier = ">=7.2" }, { name = "minio", specifier = ">=7.2" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-asgi", specifier = ">=0.45b0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.45b0" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "pillow", specifier = ">=10" }, { name = "pillow", specifier = ">=10" },
{ name = "pre-commit", specifier = ">=4.6.2" }, { name = "pre-commit", specifier = ">=4.6.2" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
@ -843,9 +697,6 @@ extract = [
{ name = "chardet", specifier = ">=5.2" }, { name = "chardet", specifier = ">=5.2" },
{ name = "mammoth", specifier = ">=1.8" }, { name = "mammoth", specifier = ">=1.8" },
{ name = "minio", specifier = ">=7.2" }, { name = "minio", specifier = ">=7.2" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "pillow", specifier = ">=10" }, { name = "pillow", specifier = ">=10" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "pymupdf", specifier = ">=1.24" }, { name = "pymupdf", specifier = ">=1.24" },
@ -861,16 +712,11 @@ notify = [
{ name = "aiosmtplib", specifier = ">=3.0" }, { name = "aiosmtplib", specifier = ">=3.0" },
{ name = "alembic", specifier = ">=1.13" }, { name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "sentry-sdk", specifier = ">=2" }, { name = "sentry-sdk", specifier = ">=2" },
{ name = "sqlalchemy", specifier = ">=2.0" }, { name = "sqlalchemy", specifier = ">=2.0" },
] ]
obs = [ obs = [
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "sentry-sdk", specifier = ">=2" }, { name = "sentry-sdk", specifier = ">=2" },
] ]
@ -879,9 +725,6 @@ prescreen = [
{ name = "alembic", specifier = ">=1.13" }, { name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "minio", specifier = ">=7.2" }, { name = "minio", specifier = ">=7.2" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.45b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
{ name = "prometheus-client", specifier = ">=0.20" }, { name = "prometheus-client", specifier = ">=0.20" },
{ name = "sentry-sdk", specifier = ">=2" }, { name = "sentry-sdk", specifier = ">=2" },
{ name = "sqlalchemy", specifier = ">=2.0" }, { name = "sqlalchemy", specifier = ">=2.0" },
@ -1104,18 +947,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
] ]
[[package]]
name = "googleapis-common-protos"
version = "1.75.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/4353a188e2c335aee33269e8b654af228278cca8e5f0b4b5f11e5d0e9adb/googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a", size = 153905, upload-time = "2026-09-03T22:31:21.869Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" },
]
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.5.5" version = "3.5.5"
@ -1155,27 +986,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
] ]
[[package]]
name = "grpcio"
version = "1.83.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" },
{ url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" },
{ url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" },
{ url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" },
{ url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" },
{ url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" },
{ url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@ -1556,190 +1366,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
] ]
[[package]]
name = "opentelemetry-api"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/45/7af37fe54e5d3e66e7dcd7ba8b8aeee73f202bfac909cc94b8c4e428f9ac/opentelemetry_exporter_otlp-1.44.0.tar.gz", hash = "sha256:af1cde7c33ea8ed624bf04ac49a885730fe44c1f1ad698656e592c38f70ce106", size = 6090, upload-time = "2026-07-16T15:25:34.585Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/c3/7b466a9463944e70b37b744072a0c1b88a425dade3fff0631adec66c9bcc/opentelemetry_exporter_otlp-1.44.0-py3-none-any.whl", hash = "sha256:4a498fa8d8fd8be9e8e2d175fe5524a3fe581ccffadd8509db86526a5fb97051", size = 6727, upload-time = "2026-07-16T15:25:14.445Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" },
]
[[package]]
name = "opentelemetry-instrumentation-asgi"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426", size = 26140, upload-time = "2026-07-16T15:25:54.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65", size = 15903, upload-time = "2026-07-16T15:24:57.198Z" },
]
[[package]]
name = "opentelemetry-instrumentation-fastapi"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97", size = 26236, upload-time = "2026-07-16T15:26:05.954Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943", size = 13261, upload-time = "2026-07-16T15:25:12.429Z" },
]
[[package]]
name = "opentelemetry-instrumentation-httpx"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/03/a529140241addd4d0acc73bafbd6f74691651b92fc0ae9b4513cf80f07fa/opentelemetry_instrumentation_httpx-0.65b0.tar.gz", hash = "sha256:4627aa9c6bb99bf4462c8b565b0ef6aeb9ffad95c6c92868be1ef7895de112ee", size = 26309, upload-time = "2026-07-16T15:26:07.973Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/0f/c6144096b4914bbf44b43ba21c962e8f333ff045770b50a3e79ed8bd455f/opentelemetry_instrumentation_httpx-0.65b0-py3-none-any.whl", hash = "sha256:400f1b78afa4ee2332b5debe58e1ed1b317913d58812c952576be76660aeadb1", size = 17436, upload-time = "2026-07-16T15:25:15.772Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" },
]
[[package]]
name = "opentelemetry-util-http"
version = "0.65b0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7", size = 11243, upload-time = "2026-07-16T15:26:27.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348", size = 8245, upload-time = "2026-07-16T15:25:46.482Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.3" version = "26.3"
@ -1894,21 +1520,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
] ]
[[package]]
name = "protobuf"
version = "7.36.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/73/f66c748df06e7fe24e658eddd600d19c4b40bad836c97ce2d0ad9851fb6b/protobuf-7.36.1.tar.gz", hash = "sha256:d0f6470f0ce2b84e3feaea2d4b816378b37ba4d4aa08a274305373de93e2d524", size = 512499, upload-time = "2026-08-31T22:40:04.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/6c/3a54a58f2948b0f485df9ecdd06590f15d0a7abf46a89d50c3de709ff4ff/protobuf-7.36.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:3cf2ee25d006cee57294a1196ea43b37feb78e0dcd1e8af5c1aeddb777655aca", size = 456046, upload-time = "2026-08-31T22:39:56.865Z" },
{ url = "https://files.pythonhosted.org/packages/6e/08/9f9548793c771095245c0eeaf0c84b76b16a5ef26158043d456f551344a0/protobuf-7.36.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:43d3d37b1eb24c113b9b7d02008cac44e423f00b611b7781ae998d7623972969", size = 344226, upload-time = "2026-08-31T22:39:58.263Z" },
{ url = "https://files.pythonhosted.org/packages/fe/51/1bdbd612fa3c51e42ea6f45d05e84dc748ddcd2663e1aa1e89a00a33facd/protobuf-7.36.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:39c518c05586c016d7874ff6079ee115bcec1ea5fbb1d177fbf7867ef4c67e44", size = 357229, upload-time = "2026-08-31T22:39:59.198Z" },
{ url = "https://files.pythonhosted.org/packages/22/df/c799fe7a05ef16ba853a59db01f3a2c5f7d0676469589ccc4874f76a2a88/protobuf-7.36.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:97198b77e369a0abd8e262b8f6c7266c55ddb796a3a12c76d7b8881188ed83aa", size = 343228, upload-time = "2026-08-31T22:40:00.179Z" },
{ url = "https://files.pythonhosted.org/packages/3d/33/d4724ec5d86d496fe4108e220618aa0837ad3423a7af7ccdd9684ccc77c8/protobuf-7.36.1-cp310-abi3-win32.whl", hash = "sha256:0b53ce95272aad50ad25d7ff03373743209822e8ba42ea7fad27d2bee1547d00", size = 443002, upload-time = "2026-08-31T22:40:01.32Z" },
{ url = "https://files.pythonhosted.org/packages/db/37/155788a0d8daded960375af604202805308169f9b859419ea0aa370946e2/protobuf-7.36.1-cp310-abi3-win_amd64.whl", hash = "sha256:51139351435d9b43d88a55eaa49fb6f737fbb478fb0cbf2cf694d1a04a9d3363", size = 456518, upload-time = "2026-08-31T22:40:02.494Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/c47f91d3cab175b01fd8c4f0d80fdf8613be876cc616e66ad281a59c5ddf/protobuf-7.36.1-py3-none-any.whl", hash = "sha256:7d951e46b3f963d6c264c367c437921de9d5aedd9c3f9612b9077736b4e3ad5c", size = 179813, upload-time = "2026-08-31T22:40:03.54Z" },
]
[[package]] [[package]]
name = "pyasn1" name = "pyasn1"
version = "0.6.4" version = "0.6.4"
@ -2223,21 +1834,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" }, { url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" },
] ]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]] [[package]]
name = "respx" name = "respx"
version = "0.23.1" version = "0.23.1"
@ -2613,59 +2209,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" },
] ]
[[package]]
name = "wrapt"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/dd/1f269e4daf0c992f675e1ca2de6b1683b761c6d0aeb6c7b4b412486823ea/wrapt-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:788e473d1a6786d29d577b1e2bd95e214c09cdafde84907c522c31069c9acfac", size = 96386, upload-time = "2026-08-30T04:40:17.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/7ecef06d33c0121c68d66a8a695efe67ebaa57218c1c61c585eca2a6117a/wrapt-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:947bd4b3438167b3638bf5477cb83a068a586ffb6d331ac427f39839c2b93b3c", size = 96532, upload-time = "2026-08-30T04:40:19.116Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e3/8fdc9eba0e6cbbfe8303e1e807d734691309a27970b2ea458d099f1a46b0/wrapt-2.4.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a69161cae7f0dca44c89c1d14146b4a0508a0c3cad98b3f2db1f4e9016c94ba", size = 228775, upload-time = "2026-08-30T04:40:20.604Z" },
{ url = "https://files.pythonhosted.org/packages/f4/77/4ac5882abfb29bf9821c5fa5cf9f30241a194e0f47faa2682b9b29765278/wrapt-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0536f5d85ff6a157ebe7e0fe08c5479943742cf1ce59569075a66159efcbc495", size = 229029, upload-time = "2026-08-30T04:40:22.186Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c5/8a3608311a02faf3e5c072da38d06a7c623150fc258e29f18fe377d91703/wrapt-2.4.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f041ed6a4d571010944bd6cfad9072db463e1851877b6d3227467a44af37456", size = 210436, upload-time = "2026-08-30T04:40:23.953Z" },
{ url = "https://files.pythonhosted.org/packages/de/90/e0cbc43f435fd39df25460e9f173e7b96f3dac5c7f66be41c7227166f021/wrapt-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7fed45dbadf5d98a52bfff9624d3cca00affeb9543d493c9632b7a53cdd35c9", size = 226586, upload-time = "2026-08-30T04:40:25.507Z" },
{ url = "https://files.pythonhosted.org/packages/81/6c/7e5f2143228635ec139ef6df733dc477049f7d96a0c49deb23944a73ed6a/wrapt-2.4.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cc2e7c7b6032e11a2b367a9baadaf0c5241feff2d8205260d87f1aa6dbdf84b", size = 208880, upload-time = "2026-08-30T04:40:27.128Z" },
{ url = "https://files.pythonhosted.org/packages/10/16/1de84402bb7a0916e10739bf6586e031244172b299e87c8cff2a04baf9ff/wrapt-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:72826910a1cf5a081234720fd43011304b899acfee219af49148155b4d795533", size = 216689, upload-time = "2026-08-30T04:40:28.844Z" },
{ url = "https://files.pythonhosted.org/packages/20/19/cd6bd5050381a541b44be97c4e0994eed60c5f439f4314f95eb5777d6c1a/wrapt-2.4.0-cp314-cp314-win32.whl", hash = "sha256:0eca69c9e93518240abe8801fb9b2726116a6e48172e4564c2651a2e14521747", size = 91581, upload-time = "2026-08-30T04:40:30.592Z" },
{ url = "https://files.pythonhosted.org/packages/a4/f8/b642f3184619adde676ad449030bcbeae6cc78ea07a92f0b5fddeec4c4e6/wrapt-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:63b94f401d7ae3a9a3027472fd3a3ff38afd2ed293b2f0b3b84a6d133a9f99a3", size = 96510, upload-time = "2026-08-30T04:40:32.1Z" },
{ url = "https://files.pythonhosted.org/packages/4d/3b/3415a18b91221261eeac85bf8ee23dfb0e2a39d76b9703a797efca177439/wrapt-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:6b3e082d43f592fcd381aee46354a11ce887a813ce5bbcedd9766fd681723c09", size = 93648, upload-time = "2026-08-30T04:40:33.563Z" },
{ url = "https://files.pythonhosted.org/packages/ac/90/80cf6a09e9599a11249775928df9bb790b82471e4312b847a861ffb2c2ed/wrapt-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09064c7be688c38c3ff125ce86bc26b69b5d78dd56062c3ddd9c814b2a25f1e1", size = 99615, upload-time = "2026-08-30T04:40:35.134Z" },
{ url = "https://files.pythonhosted.org/packages/b2/da/c1d3245abb911a42584f8f7e9781995bdc41345c7affba75cf7e376c85ac/wrapt-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4f8ddff4bbb75916be36da5169b8b9d475b59a1bd24acdb7551bb2c71be9aaac", size = 100031, upload-time = "2026-08-30T04:40:36.641Z" },
{ url = "https://files.pythonhosted.org/packages/84/46/8ec4941d0abbb010df7caf0a34840ca0128177389843b0f5ef2f9ee48ac5/wrapt-2.4.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f8017443595870aa31f46125553a5c55ce95a26a267b96261baee6ba566d83", size = 269389, upload-time = "2026-08-30T04:40:38.212Z" },
{ url = "https://files.pythonhosted.org/packages/14/b5/a0ae1b431cc1f49a545d32b8b678a5788c50583ecf0ecb85dc0c7f95b4f6/wrapt-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:328eb2d978ca3a6ae25f8d8fe560bf8f4bc9778b5932e7b142664eef05b92e8f", size = 281081, upload-time = "2026-08-30T04:40:40.045Z" },
{ url = "https://files.pythonhosted.org/packages/c7/24/dfaf53dd3bdb0703524a9367b48e2a64ea86433fcc854b5f14be6a8e0e39/wrapt-2.4.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a057d376d994da6bd1bbf955ecfda699aa7353826f98847f5605e1801abdfd4", size = 249637, upload-time = "2026-08-30T04:40:41.657Z" },
{ url = "https://files.pythonhosted.org/packages/3e/27/bdd82044d7503c2bfa78afcc89881f82a1b82b5d2013aabab853d339ce2a/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3367a5212212c9393e0d3ca6ae029b3a8fa40c5896e4a985d43fe8a4b8322f0d", size = 275322, upload-time = "2026-08-30T04:40:43.408Z" },
{ url = "https://files.pythonhosted.org/packages/c4/82/04f4228eb3fb348d660dd1ea7225e53665b1809df2273ff4861d4d33b741/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c4fca1e63af6675af3df7cdfcd5a0c878b5e655c7e48611ced9dc8d62183a11d", size = 247292, upload-time = "2026-08-30T04:40:45.457Z" },
{ url = "https://files.pythonhosted.org/packages/a2/20/67b2968fa9200458446c51b36a435adb6906083428b70fafb4caf92d4dc2/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:694005fdc3002ade0f21641408c588028abde03c85961f3ba7727d8bead3ed6b", size = 264586, upload-time = "2026-08-30T04:40:47.079Z" },
{ url = "https://files.pythonhosted.org/packages/d7/fd/0db9ba03e08a7663f52455e95520c723f567bc037bffc6699950fcc456c4/wrapt-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:332d9bad7e9b718974bb2a576504c4956f45b4a0fcd7b3bb7827279167550464", size = 93752, upload-time = "2026-08-30T04:40:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/3f/87/ced171220935c696b157207385fa6be5675558a74655479f071d95a00f1d/wrapt-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d57264c9dfcf37d2bf0b0fbec68d0f6184fc5617267619ada04d03e8b0231f3", size = 99890, upload-time = "2026-08-30T04:40:50.407Z" },
{ url = "https://files.pythonhosted.org/packages/a3/af/4a10c9a6d3b7ae41f830978c28d33a59ceb29537bd6875d2abfe78db4b41/wrapt-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f43af38a642c3d6062e9740d8f5cc0feb5dbe0da516702df892147393b8cb14d", size = 96033, upload-time = "2026-08-30T04:40:51.933Z" },
{ url = "https://files.pythonhosted.org/packages/a0/df/3a0b6225ab88bd47090df70391c059a3308057638f8fc0ae32e8ac9d1886/wrapt-2.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:430fde1a116df3ceb5c29035de1da6609b70e680d9b8ce3ee624422f3fe0978c", size = 96389, upload-time = "2026-08-30T04:40:53.555Z" },
{ url = "https://files.pythonhosted.org/packages/ea/6f/803b0d0e14de11781f0e938e6f7d6e29e79652139fe70d7513460357ac78/wrapt-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7d28f8f35a02d49f75f57fa4e755db4ba33f65841c0de64cd65b253916f5bf06", size = 96557, upload-time = "2026-08-30T04:40:55.033Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e8/46571e1218d0494604a7aadc4c898c738c4b179052327ee1e57e278cebd6/wrapt-2.4.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd9a4be6785295e471f71efdf5682bd11d5b822b9665e6e1b4844917cf2f7ac", size = 229230, upload-time = "2026-08-30T04:40:56.703Z" },
{ url = "https://files.pythonhosted.org/packages/78/2e/0cab15fcaec56096a5734feace3620bc01edc885653be04bd756f84a6784/wrapt-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75529a2fb569a671cf162f762c1b576f569f571b55ec7f3481258ca842ba507f", size = 229444, upload-time = "2026-08-30T04:40:58.51Z" },
{ url = "https://files.pythonhosted.org/packages/e7/9e/a92c049371a2675f98a0381ab2951f984866d1ba4de0e0771d6a31fdaa2b/wrapt-2.4.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66e7512c0d324cc37bba1def2be1fc365cbb685d3aa393a8f6f4d2d00202881d", size = 212482, upload-time = "2026-08-30T04:41:00.224Z" },
{ url = "https://files.pythonhosted.org/packages/ee/3b/8b5b57d0ff24edcd3421dbaeb4e94c89be3616824e47708f4e13f25ae3d7/wrapt-2.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5f3bdfc35c83b562fcaebc0f24593045e5ed9f3b633adafd35222718a0ec38fa", size = 227017, upload-time = "2026-08-30T04:41:01.918Z" },
{ url = "https://files.pythonhosted.org/packages/0e/20/124b40bfd9585848db5a5aa6741d0c8dbf378dd995c6c2d95f090d9cf540/wrapt-2.4.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5f45bead708e2c0014be5e98531ce7202916b098a208c7be83c6ceb0a2559fa", size = 210498, upload-time = "2026-08-30T04:41:03.617Z" },
{ url = "https://files.pythonhosted.org/packages/4b/bf/89db9d5a80a9f2af52b24bdfdb5392be80bc0f0fd39fc39d1aab72afd0bd/wrapt-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d294576fddac636589e4deccfe782e8f429da10f167c1985c4d51071de3672b7", size = 217046, upload-time = "2026-08-30T04:41:05.473Z" },
{ url = "https://files.pythonhosted.org/packages/3b/0b/021c9d6ce64c639894bffdaa7a895ddd4187abfefb2873ce55e536cd9d56/wrapt-2.4.0-cp315-cp315-win32.whl", hash = "sha256:0191d717dfbb8e519e7bfd4775e5b9bd57e359b3a09ab5db1ea47f6025b4d845", size = 91591, upload-time = "2026-08-30T04:41:07.086Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d3/6ebd944041cea0ac4a108a4739510ed2dc891a3f3216e4f7bf0650f5b5a6/wrapt-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:e8df31a126a0a247c1aa379e30873839de03912dea09ca360c680f3625d815df", size = 96517, upload-time = "2026-08-30T04:41:08.671Z" },
{ url = "https://files.pythonhosted.org/packages/96/84/7c5e52e450f80ba76fd0282dccf7c79cd004ebd8ccabd0903064d3d2c56e/wrapt-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:e9e7e94472f0e3f1447caf27e1939eb384d0e87972a35a05f5c2e0968e9c01af", size = 93652, upload-time = "2026-08-30T04:41:10.258Z" },
{ url = "https://files.pythonhosted.org/packages/35/89/f08ff45d7646de29750932805cc3b1e86b6ac3128015b293ed45fa8efe86/wrapt-2.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8828369b7d3e93c547cc8ad931b5a57b4e8d174035c82762fb1091e7d05ac9f5", size = 99610, upload-time = "2026-08-30T04:41:11.933Z" },
{ url = "https://files.pythonhosted.org/packages/4b/c2/f9a3c40901a36c6bb7ecaff8e1e54af78fa7fa0b95a0e54d13d3a24c8a0a/wrapt-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:413e757dce7a43fcda8bb8441994b1127492ffac6a5803af777d44516df8c6e2", size = 100064, upload-time = "2026-08-30T04:41:13.492Z" },
{ url = "https://files.pythonhosted.org/packages/0b/e1/e2437f17f2a1ec292056e2fcafe1248269ebc39502f2ffe79424bf86f8a6/wrapt-2.4.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:75944792cf6b99262d649d55710bf5901f7013fbb212c7a1d736b97a20517607", size = 269421, upload-time = "2026-08-30T04:41:15.238Z" },
{ url = "https://files.pythonhosted.org/packages/c6/d0/c98d6548dc4c7d12ab9baa192234ca1a57e141afd283252b448faddbd9ef/wrapt-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:648d1d4f94e8a0a1656675c755f40d2f0ee5fe92c449ab45326f4ecc2738cbe8", size = 281452, upload-time = "2026-08-30T04:41:16.939Z" },
{ url = "https://files.pythonhosted.org/packages/a3/57/673168e00aa03725148ce621ed201b75df4e787a57acd48fecefd2725600/wrapt-2.4.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a112a1bfdd2621e4344cb0a32dbaab80636b32dac1b055d03fbb2a67d806d1db", size = 250358, upload-time = "2026-08-30T04:41:18.716Z" },
{ url = "https://files.pythonhosted.org/packages/78/0b/f2e576de5bf53ef5b578470104ea93f33e273a704c825131bc1719fffc42/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0972cd025f4c86fa2d8abd953d9f875779935343af58b4ce019ff89573fc65bd", size = 275654, upload-time = "2026-08-30T04:41:20.418Z" },
{ url = "https://files.pythonhosted.org/packages/33/7f/9347b2e236346b1ba4cb28b82b205b8a377bb2da9417cb81bbe3d25816d7/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c246aaed719dcdb62eeb7b8d9306a6237777226ef3baad35919c4ae134c91ce7", size = 248662, upload-time = "2026-08-30T04:41:22.371Z" },
{ url = "https://files.pythonhosted.org/packages/a5/36/3b84d9e1ac8393bf2c94272760a2d361dc394ac30301e6d6dbd6583ade2d/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1656de3835f760781c9b974bce07d8c04edb9c9ad7ad67264aee69cd68a1db09", size = 264813, upload-time = "2026-08-30T04:41:24.116Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a2/de7b1de1702667b4a048318e301e26887268c17b07c8b9797cea06b10aee/wrapt-2.4.0-cp315-cp315t-win32.whl", hash = "sha256:d8e6e1e5dc684dfce7c33fc8b67a08ba2af94f3a45cfc70d5c1d6a839d2caf97", size = 93753, upload-time = "2026-08-30T04:41:25.793Z" },
{ url = "https://files.pythonhosted.org/packages/09/50/4e7ef58c4eb058861ceddc0d1f94a6ed87f62e1cb27783c60b2897ef7e58/wrapt-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:85ed3c67fd39e8d9a36c224758cb6f2f4eb277d07ea677930caa0008c18ec002", size = 99888, upload-time = "2026-08-30T04:41:27.305Z" },
{ url = "https://files.pythonhosted.org/packages/68/64/d15740c763dd0ddea2338ad42e3bd4a84f8702e16083e7ff61674c504a13/wrapt-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:36b56a4fba13b34ed8ff307557325fff215de0a58b5dbaef2c50e4d8aa39dbd1", size = 96039, upload-time = "2026-08-30T04:41:29.062Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" },
]
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.24.5" version = "1.24.5"