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:
parent
dc653737e0
commit
b279d6c61a
25 changed files with 364 additions and 904 deletions
32
.env.example
32
.env.example
|
|
@ -48,22 +48,18 @@ REFUND_FULL_USAGE_THRESHOLD=0.20 # ≤20% consumed + within window ⇒ full ref
|
|||
|
||||
# --- Observability (leave empty to disable) ---
|
||||
SENTRY_DSN=
|
||||
# OTLP endpoint for traces/metrics/logs.
|
||||
# profile: observer → http://otel-collector:4318 (OTLP/HTTP via collector)
|
||||
# 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
|
||||
# Auth header for OpenObserve. Used by Vector in the `observer` profile to forward
|
||||
# logs and metrics. Generate with: echo -n 'user:pass' | base64
|
||||
# Default value is for root@example.com:Complexpass#123.
|
||||
OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
|
||||
# Only needed if sending OTLP directly to OpenObserve (without collector).
|
||||
# The collector uses OPENOBSERVE_AUTH_TOKEN instead.
|
||||
OTEL_EXPORTER_OTLP_HEADERS=
|
||||
OTEL_SERVICE_NAME=contract-check
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT is no longer used. The application does not push
|
||||
# OTLP; Vector/OpenObserve collects logs/metrics passively from stdout and
|
||||
# /metrics endpoints. Remove this line from existing .env files.
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=
|
||||
|
||||
# --- OpenObserve (profile: observer) ---
|
||||
OPENOBSERVE_PORT=5080
|
||||
OPENOBSERVE_GRPC_PORT=5081
|
||||
OPENOBSERVE_ROOT_USER_EMAIL=root@example.com
|
||||
OPENOBSERVE_ROOT_USER_PASSWORD=Complexpass#123
|
||||
# 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_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_PORT=3000
|
||||
GRAFANA_ADMIN_USER=admin
|
||||
|
|
@ -120,9 +111,16 @@ CHUNK_SIZE_CHARS=10000
|
|||
# --- API (FastAPI) ---
|
||||
API_HOST=0.0.0.0
|
||||
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
|
||||
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=
|
||||
CORS_ORIGINS= # comma-separated, future web SPA
|
||||
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -132,13 +132,13 @@ obs-url: ## Print Grafana URL and default credentials
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
|
||||
observer-down: ## Stop OpenObserve + otel-collector containers
|
||||
observer-down: ## Stop OpenObserve + Vector containers
|
||||
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
|
||||
|
||||
obs-reset: ## Reset Grafana and Loki volumes (wipes dashboards/logs data)
|
||||
|
|
|
|||
|
|
@ -143,11 +143,12 @@ docker compose --profile bot up -d --build # + bot (можно запус
|
|||
- `services` — api + worker-ы (без бота).
|
||||
- `bot` — Telegram-бот; можно поднять на этом же хосте или на отдельном сервере (`docs/DEPLOY.md` §14).
|
||||
- `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`,
|
||||
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`.
|
||||
RabbitMQ AMQP `5672` / UI `15672`, MinIO `9000` / console `9001`, api `8000` / metrics `9100`
|
||||
(по умолчанию только на loopback), OpenObserve UI `5080`, edge `80`/`443`.
|
||||
Метрики worker-ов больше не публикуются на хост; их скрейпит Vector внутри сети compose.
|
||||
|
||||
## API (кратко)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
115
deploy/observability/vector-config.yaml
Normal file
115
deploy/observability/vector-config.yaml
Normal 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
|
||||
|
|
@ -150,10 +150,9 @@ services:
|
|||
REDIS_URL: redis://redis:6379/0
|
||||
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
||||
S3_ENDPOINT_URL: http://minio:9000
|
||||
OTEL_SERVICE_NAME: api
|
||||
ports:
|
||||
- "${API_PORT:-8000}:8000"
|
||||
- "${API_METRICS_PORT:-9100}:9100"
|
||||
- "${API_METRICS_BIND_HOST:-127.0.0.1}:${API_METRICS_PORT:-9100}:9100"
|
||||
healthcheck:
|
||||
test:
|
||||
- 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}
|
||||
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
||||
S3_ENDPOINT_URL: http://minio:9000
|
||||
OTEL_SERVICE_NAME: worker-extract
|
||||
ports:
|
||||
- "9101:9101"
|
||||
|
||||
worker-analyze:
|
||||
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}
|
||||
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
||||
S3_ENDPOINT_URL: http://minio:9000
|
||||
OTEL_SERVICE_NAME: worker-analyze
|
||||
ports:
|
||||
- "9102:9102"
|
||||
|
||||
worker-prescreen:
|
||||
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}
|
||||
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
||||
S3_ENDPOINT_URL: http://minio:9000
|
||||
OTEL_SERVICE_NAME: worker-prescreen
|
||||
ports:
|
||||
- "9104:9104"
|
||||
|
||||
worker-billing:
|
||||
profiles: ["services"]
|
||||
|
|
@ -267,9 +257,6 @@ services:
|
|||
required: false
|
||||
environment:
|
||||
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:
|
||||
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}
|
||||
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
||||
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
|
||||
# the bot holds no DB/MQ/S3 credentials — it talks only to the API.
|
||||
|
|
@ -399,16 +383,15 @@ services:
|
|||
# ── OBSERVABILITY ──────────────────────────────────────────────────────────
|
||||
# Two observability stacks are provided; activate exactly one profile.
|
||||
#
|
||||
# profile: observer → OpenObserve (lightweight, single binary).
|
||||
# Use for dev/homelab or when hardware is tight.
|
||||
# profile: observer → OpenObserve via Vector (lightweight, single binary).
|
||||
# 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).
|
||||
# Use for production-grade visibility.
|
||||
#
|
||||
# App services send OTLP traces/metrics/logs to the endpoint configured in
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT (.env). Point it at otel-collector:4318 for the
|
||||
# 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.
|
||||
# For the `observer` profile, set OPENOBSERVE_AUTH_TOKEN so Vector can write
|
||||
# to OpenObserve. No OTLP endpoint configuration is needed in the app.
|
||||
|
||||
# ── LIGHTWEIGHT OBSERVABILITY (profile: observer) ───────────────────────────
|
||||
openobserve:
|
||||
|
|
@ -429,8 +412,7 @@ services:
|
|||
volumes:
|
||||
- openobserve-data:/data
|
||||
ports:
|
||||
- "${OPENOBSERVE_PORT:-5080}:5080" # UI + OTLP/HTTP
|
||||
- "${OPENOBSERVE_GRPC_PORT:-5081}:5081" # OTLP/gRPC
|
||||
- "${OPENOBSERVE_PORT:-5080}:5080" # UI + ingestion endpoints
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
|
@ -440,20 +422,17 @@ services:
|
|||
cpus: "${OPENOBSERVE_CPU_RESERVATION:-0.5}"
|
||||
memory: "${OPENOBSERVE_MEMORY_RESERVATION:-512M}"
|
||||
|
||||
otel-collector:
|
||||
vector:
|
||||
profiles: ["observer"]
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
container_name: contract_check-otel-collector
|
||||
image: timberio/vector:0.43.0-alpine
|
||||
container_name: contract_check-vector
|
||||
restart: unless-stopped
|
||||
command: ["--config", "/etc/otelcol/config.yaml"]
|
||||
environment:
|
||||
OPENOBSERVE_AUTH_TOKEN: ${OPENOBSERVE_AUTH_TOKEN}
|
||||
METRICS_BEARER_TOKEN: ${METRICS_BEARER_TOKEN:-}
|
||||
volumes:
|
||||
- ./deploy/observability/otel-collector-config.yaml:/etc/otelcol/config.yaml:ro
|
||||
ports:
|
||||
- "${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
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./deploy/observability/vector-config.yaml:/etc/vector/vector.yaml:ro
|
||||
depends_on:
|
||||
openobserve:
|
||||
condition: service_started
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
> **workers** (extract/OCR, deterministic prescreen, and LLM-analyze, split for
|
||||
> CPU vs I/O profiles), a Telegram **bot** adapter, backed by
|
||||
> **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
|
||||
> (`srv/` — api + three workers + bot + prototype), shared `contract_check.core`
|
||||
> 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
|
||||
a failure-class taxonomy; credit refund stays idempotent.
|
||||
- **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`),
|
||||
webhook-driven state machine, plans/subscriptions + credit top-ups, and a
|
||||
`worker-billing` scheduler (renewals/expiry/reconciliation). Disabled by
|
||||
|
|
@ -129,7 +130,7 @@ Control plane:
|
|||
| Extra tables | jobs, service_tokens, invoices(stub) | 3-table plan |
|
||||
| Report storage | JSONB + markdown column | — |
|
||||
| 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 |
|
||||
| Refund | Policy switch `all\|infra_only` + failure classes | refund-all |
|
||||
| Edge | Nginx + certbot | — |
|
||||
|
|
@ -167,7 +168,7 @@ DealDocumentScreening/
|
|||
│ │ └── certbot-init.sh (initial cert + nginx reload)
|
||||
│ └── observability/
|
||||
│ ├── 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)
|
||||
│ ├── tempo.yaml (planned — trace storage for Grafana stack)
|
||||
│ └── grafana/provisioning/
|
||||
|
|
@ -185,7 +186,7 @@ DealDocumentScreening/
|
|||
│ │ ├── __init__.py
|
||||
│ │ ├── config.py (pydantic-settings: base + per-service, §11)
|
||||
│ │ ├── 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)
|
||||
│ │ ├── metrics.py (prometheus_client registry + counters/hists)
|
||||
│ │ ├── api_keys.py (B2B key gen/hash/verify — sha256 + hmac.compare_digest)
|
||||
|
|
@ -1028,7 +1029,7 @@ change.
|
|||
### Volumes & restart
|
||||
|
||||
- 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.
|
||||
- `depends_on: condition: service_healthy` everywhere with real healthchecks
|
||||
(pg `pg_isready`, rabbit `rabbitmq-diagnostics ping`, minio `mc ready`,
|
||||
|
|
@ -1086,12 +1087,10 @@ None of 1–5 requires touching `core/` application code — only compose/infra.
|
|||
| `REFUND_WINDOW_DAYS` | `14` | full-refund window (§8a) |
|
||||
| `REFUND_FULL_USAGE_THRESHOLD` | `0.20` | usage ratio for full refund |
|
||||
| `SENTRY_DSN` | (empty) | if set, init sentry |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | (empty) | OTLP endpoint; with profile `observer` use `http://otel-collector:4318` |
|
||||
| `OTEL_SERVICE_NAME` | per-service | overridden in each service settings |
|
||||
| `OPENOBSERVE_AUTH_TOKEN` | default creds base64 | Basic auth token the collector uses to forward to OpenObserve |
|
||||
| `OTEL_COLLECTOR_OTLP_GRPC_PORT` | `4317` | host port for collector OTLP/gRPC receiver |
|
||||
| `OTEL_COLLECTOR_OTLP_HTTP_PORT` | `4318` | host port for collector OTLP/HTTP receiver |
|
||||
| `OTEL_COLLECTOR_METRICS_PORT` | `8889` | host port for collector self-metrics |
|
||||
| `OPENOBSERVE_AUTH_TOKEN` | default creds base64 | Basic auth token Vector uses to forward logs/metrics to OpenObserve |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | (removed) | no longer used; the application does not push OTLP |
|
||||
| `OTEL_EXPORTER_OTLP_HEADERS` | (removed) | no longer used |
|
||||
| `OTEL_SERVICE_NAME` | (removed) | service name is set by `configure_logging()` and compose labels |
|
||||
| `GRAFANA_PORT` | `3000` | host port for the Grafana UI (profile `obs`) |
|
||||
| `GRAFANA_ADMIN_USER` | `admin` | initial Grafana admin user |
|
||||
| `GRAFANA_ADMIN_PASSWORD` | `admin` | initial Grafana admin password |
|
||||
|
|
@ -1141,9 +1140,10 @@ None of 1–5 requires touching `core/` application code — only compose/infra.
|
|||
|---|---|---|
|
||||
| `API_HOST` | `0.0.0.0` | |
|
||||
| `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` |
|
||||
| `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) | |
|
||||
|
||||
### Auth (JWT + Telegram identity verification + webUI + admin panel)
|
||||
|
|
@ -1249,23 +1249,19 @@ dependencies = [
|
|||
db = ["sqlalchemy>=2.0", "asyncpg>=0.29", "alembic>=1.13"]
|
||||
mq = ["aio-pika>=9.4"]
|
||||
s3 = ["minio>=7.2"]
|
||||
obs = ["prometheus-client>=0.20", "sentry-sdk>=2",
|
||||
"opentelemetry-sdk>=1.24", "opentelemetry-exporter-otlp>=1.24"]
|
||||
obs = ["prometheus-client>=0.20", "sentry-sdk>=2"]
|
||||
|
||||
api = [{ include-group = "db" }, { include-group = "mq" },
|
||||
{ include-group = "s3" }, { include-group = "obs" },
|
||||
"fastapi>=0.110", "uvicorn[standard]>=0.29", "python-multipart>=0.0.9",
|
||||
"redis>=5.0",
|
||||
"opentelemetry-instrumentation-fastapi>=0.45b0",
|
||||
"opentelemetry-instrumentation-asgi>=0.45b0"]
|
||||
"redis>=5.0"]
|
||||
extract = [{ include-group = "db" }, { include-group = "mq" },
|
||||
{ include-group = "s3" }, { include-group = "obs" },
|
||||
"pymupdf>=1.24", "pytesseract>=0.3.10", "pillow>=10",
|
||||
"mammoth>=1.8", "striprtf>=0.0.26", "chardet>=5.2",
|
||||
"python-magic>=0.4.27"]
|
||||
analyze = [{ include-group = "db" }, { include-group = "mq" },
|
||||
{ include-group = "s3" }, { include-group = "obs" },
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0"]
|
||||
{ include-group = "s3" }, { include-group = "obs" }]
|
||||
prescreen = [{ include-group = "db" }, { include-group = "mq" },
|
||||
{ include-group = "s3" }, { include-group = "obs" }]
|
||||
bot = ["aiogram>=3.4"]
|
||||
|
|
@ -1283,8 +1279,8 @@ One file, profiles. Default (`docker compose up`) = infra only.
|
|||
- `services` profile = api + workers (no bot).
|
||||
- `bot` profile = Telegram bot adapter; can run on the same host or a separate
|
||||
server (`docs/DEPLOY.md` §14).
|
||||
- `obs` (Grafana + Loki + Prometheus) and `observer` (OpenObserve + OTel
|
||||
Collector) are mutually exclusive observability stacks.
|
||||
- `obs` (Grafana + Loki + Prometheus) and `observer` (OpenObserve + Vector)
|
||||
are mutually exclusive observability stacks.
|
||||
- `edge` = Nginx + certbot.
|
||||
|
||||
```
|
||||
|
|
@ -1316,9 +1312,9 @@ services:
|
|||
prometheus: prom/prometheus; scrapes service /metrics endpoints
|
||||
|
||||
# ── OBSERVABILITY — OPENOBSERVE STACK (profile: observer) ──
|
||||
openobserve: public.ecr.aws/zinclabs/openobserve:latest; UI + OTLP backend on :5080, gRPC on :5081
|
||||
otel-collector: otel/opentelemetry-collector-contrib:latest; OTLP receiver 4317/4318,
|
||||
scrapes /metrics from services, forwards logs/metrics/traces to OpenObserve
|
||||
openobserve: public.ecr.aws/zinclabs/openobserve:latest; UI + JSON/Prometheus-write backend on :5080
|
||||
vector: timberio/vector:0.43.0-alpine; reads Docker logs + scrapes /metrics,
|
||||
forwards logs/metrics to OpenObserve
|
||||
|
||||
# ── EDGE (profile: edge) ──
|
||||
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
|
||||
contextvar before handling. So a single upload's logs trace
|
||||
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,
|
||||
so you can click through from any log line to every other log line with the
|
||||
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`
|
||||
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,
|
||||
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
|
||||
`4317` (gRPC) and `4318` (HTTP).
|
||||
- Services export OTLP traces, metrics, and logs to `http://otel-collector:4318`
|
||||
(`OTEL_EXPORTER_OTLP_ENDPOINT`).
|
||||
- The collector also scrapes Prometheus `/metrics` from `api:9100` and each
|
||||
worker (`:9101`/`:9102`/`:9104`/`:9105`/`:9103`).
|
||||
- `otlphttp/openobserve` exporter forwards logs and traces to
|
||||
`http://openobserve:5080/openobserve/api/default` using `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.
|
||||
- `vector` runs in the `observer` profile. It reads container logs from the
|
||||
local Docker socket and scrapes Prometheus `/metrics` endpoints inside the
|
||||
compose network (`api:8000/metrics`, workers on `:9101`/`:9102`/`:9104`/`:9105`/`:9103`).
|
||||
- Vector enriches logs with `service` (container name) and preserves
|
||||
`correlation_id` from JSON log lines.
|
||||
- Logs are sent to `http://openobserve:5080/openobserve/api/default/_json` and
|
||||
metrics to `http://openobserve:5080/openobserve/api/default/prometheus/api/v1/write`,
|
||||
both authenticated with `OPENOBSERVE_AUTH_TOKEN`.
|
||||
- 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.
|
||||
- **Publisher confirms** on; a published-but-unconfirmed message fails the
|
||||
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).
|
||||
- React SPA + Telegram Login + SSE/webhook delivery.
|
||||
- Grafana dashboards polished; Tempo/Jaeger trace UI.
|
||||
- Grafana dashboards polished.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -144,9 +144,10 @@ PRICE_PER_DOC_KOPECKS=19900 # 199 ₽ за документ без подпис
|
|||
|
||||
# --- Observability (опционально) ---
|
||||
SENTRY_DSN=https://...@sentry.io/...
|
||||
# OTLP/HTTP endpoint. With profile `observer` use the local collector.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
||||
# OpenObserve auth token used by Vector in the `observer` profile.
|
||||
# Generate with: echo -n 'user:pass' | base64
|
||||
OPENOBSERVE_AUTH_TOKEN=cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT больше не используется: приложение не шлёт OTLP.
|
||||
```
|
||||
|
||||
> Полный список: см. `.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` |
|
||||
| `bot` | `bot` — Telegram-адаптер; можно поднять на этом же хосте или на отдельном сервере |
|
||||
| `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` |
|
||||
|
||||
```bash
|
||||
|
|
@ -220,7 +221,7 @@ docker compose --profile services --profile bot up -d --build
|
|||
# + observability Grafana/Loki/Prometheus (логи + метрики):
|
||||
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
|
||||
|
||||
# + edge (nginx reverse proxy + TLS; deploy/nginx готов):
|
||||
|
|
@ -305,7 +306,9 @@ docker compose --profile bot logs -f bot
|
|||
```bash
|
||||
curl http://localhost:8000/healthz # liveness — 200
|
||||
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
|
||||
|
|
@ -630,7 +633,8 @@ curl https://contract-check.example.com/healthz
|
|||
По умолчанию `nginx.conf` проксирует:
|
||||
- `/api/v1/*`, `/admin/*`
|
||||
- `/healthz`, `/readyz`
|
||||
- `/metrics` (открыт наружу; закройте файрволом или уберите приватный скрейпинг)
|
||||
- `/metrics` → проксируется наружу; настройте `METRICS_BEARER_TOKEN` и ограничьте доступ
|
||||
файрволом/edge, чтобы метрики не утекали наружу. Worker-метрики доступны только внутри сети compose.
|
||||
- `/webhook/*` → `/api/v1/webhooks/`
|
||||
- `${BOT_WEBHOOK_PATH}` → `bot:8080` (Telegram-бот в webhook-режиме, профиль `bot`; см. §14.4)
|
||||
- `/grafana/*` → `grafana:3000` (когда поднят профиль `obs`)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ packages = ["src/contract_check"]
|
|||
#
|
||||
# api → core + db/mq/s3/obs + fastapi/uvicorn
|
||||
# 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)
|
||||
# dev → everything needed to lint/typecheck/test locally
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -51,8 +51,6 @@ s3 = [
|
|||
obs = [
|
||||
"prometheus-client>=0.20",
|
||||
"sentry-sdk>=2",
|
||||
"opentelemetry-sdk>=1.24",
|
||||
"opentelemetry-exporter-otlp>=1.24",
|
||||
]
|
||||
api = [
|
||||
{ include-group = "db" },
|
||||
|
|
@ -68,9 +66,6 @@ api = [
|
|||
"argon2-cffi>=23.1",
|
||||
"email-validator>=2.1",
|
||||
"webauthn>=2.5",
|
||||
"opentelemetry-instrumentation-fastapi>=0.45b0",
|
||||
"opentelemetry-instrumentation-asgi>=0.45b0",
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
]
|
||||
extract = [
|
||||
{ include-group = "db" },
|
||||
|
|
@ -84,21 +79,18 @@ extract = [
|
|||
"striprtf>=0.0.26",
|
||||
"chardet>=5.2",
|
||||
"python-magic>=0.4.27",
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
]
|
||||
analyze = [
|
||||
{ include-group = "db" },
|
||||
{ include-group = "mq" },
|
||||
{ include-group = "s3" },
|
||||
{ include-group = "obs" },
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
]
|
||||
prescreen = [
|
||||
{ include-group = "db" },
|
||||
{ include-group = "mq" },
|
||||
{ include-group = "s3" },
|
||||
{ include-group = "obs" },
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
]
|
||||
bot = [
|
||||
"aiogram>=3.4",
|
||||
|
|
@ -112,12 +104,10 @@ notify = [
|
|||
{ include-group = "mq" },
|
||||
{ include-group = "obs" },
|
||||
"aiosmtplib>=3.0",
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
]
|
||||
billing = [
|
||||
{ include-group = "db" },
|
||||
{ include-group = "obs" },
|
||||
"opentelemetry-instrumentation-httpx>=0.45b0",
|
||||
"pyjwt>=2.8",
|
||||
]
|
||||
dev = [
|
||||
|
|
|
|||
|
|
@ -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.s3.minio_storage import MinioStorage
|
||||
from src.contract_check.core.sentry import init_sentry
|
||||
from src.contract_check.core.telemetry import setup_telemetry, shutdown_telemetry
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
|
@ -55,7 +54,6 @@ async def lifespan(app: FastAPI) -> Any:
|
|||
)
|
||||
bind_context(service="api", env=settings.env)
|
||||
init_sentry("api")
|
||||
setup_telemetry("api")
|
||||
|
||||
storage = MinioStorage.from_endpoint_url(
|
||||
endpoint_url=settings.s3_endpoint_url,
|
||||
|
|
@ -112,7 +110,6 @@ async def lifespan(app: FastAPI) -> Any:
|
|||
await redis_client.aclose()
|
||||
except Exception:
|
||||
pass
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
|
|
@ -147,15 +144,6 @@ def create_app() -> FastAPI:
|
|||
) -> RedirectResponse:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ Readiness-проба. Выполняет `SELECT 1` в БД.
|
|||
|
||||
## metrics
|
||||
|
||||
Файл: [`metrics.py`](metrics.py). Тег: `metrics`. Без аутентификации.
|
||||
Файл: [`metrics.py`](metrics.py). Тег: `metrics`. Без аутентификации, если
|
||||
`METRICS_BEARER_TOKEN` не задан; иначе требуется `Authorization: Bearer <token>`.
|
||||
|
||||
### `GET /metrics`
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Adapters (bot) deliberately do NOT import this package beyond config/logging
|
|||
Subpackages:
|
||||
config — typed env config (pydantic-settings)
|
||||
logging — structlog JSON + correlation_id propagation
|
||||
telemetry — OpenTelemetry init
|
||||
telemetry — no-op stubs; OTLP push removed
|
||||
sentry — Sentry init
|
||||
metrics — Prometheus counters/histograms + metrics HTTP server
|
||||
db — SQLAlchemy 2 models (6 tables), async session, enums
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ def _jwt_refresh_ttl() -> dt.timedelta:
|
|||
|
||||
def create_access_token(user_id: uuid.UUID, telegram_id: int) -> str:
|
||||
"""Sign a fresh access JWT for a verified user."""
|
||||
settings = get_settings()
|
||||
now = dt.datetime.now(tz=dt.UTC)
|
||||
claims = AccessTokenClaims(
|
||||
sub=user_id,
|
||||
|
|
@ -111,7 +110,7 @@ def create_access_token(user_id: uuid.UUID, telegram_id: int) -> str:
|
|||
{
|
||||
"iat": int(now.timestamp()),
|
||||
"exp": int((now + _jwt_access_ttl()).timestamp()),
|
||||
"iss": settings.otel_service_name or "contract-check",
|
||||
"iss": "contract-check",
|
||||
"aud": "contract-check",
|
||||
}
|
||||
)
|
||||
|
|
@ -182,7 +181,6 @@ class RefreshTokenClaims:
|
|||
|
||||
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."""
|
||||
settings = get_settings()
|
||||
now = dt.datetime.now(tz=dt.UTC)
|
||||
claims = RefreshTokenClaims(sub=user_id, jti=jti, type=JWT_TYPE_REFRESH)
|
||||
payload = claims.to_dict()
|
||||
|
|
@ -190,7 +188,7 @@ def create_refresh_token(user_id: uuid.UUID, jti: str) -> str:
|
|||
{
|
||||
"iat": int(now.timestamp()),
|
||||
"exp": int((now + _jwt_refresh_ttl()).timestamp()),
|
||||
"iss": settings.otel_service_name or "contract-check",
|
||||
"iss": "contract-check",
|
||||
"aud": "contract-check",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,8 +86,6 @@ class Settings(BaseSettings):
|
|||
|
||||
# --- observability (empty disables) ---
|
||||
sentry_dsn: str = ""
|
||||
otel_exporter_otlp_endpoint: str = ""
|
||||
otel_service_name: str = "contract-check"
|
||||
|
||||
# --- LLM provider ---
|
||||
llm_provider: str = "ollama_cloud"
|
||||
|
|
|
|||
|
|
@ -183,8 +183,8 @@ def configure_logging(
|
|||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
# Route stdlib logging through structlog so third-party libs match our format,
|
||||
# and structlog events reach any OTEL handlers attached to the root logger.
|
||||
# Route stdlib logging through structlog so third-party libs match our format.
|
||||
# Structlog events always reach the root StreamHandler configured below.
|
||||
stdlib_handler = logging.StreamHandler(sys.stderr)
|
||||
stdlib_handler.setFormatter(
|
||||
structlog.stdlib.ProcessorFormatter(
|
||||
|
|
|
|||
|
|
@ -28,4 +28,4 @@ def init_sentry(service_name: str | None = None) -> None:
|
|||
traces_sample_rate=traces_sample_rate,
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,167 +1,117 @@
|
|||
"""OpenTelemetry initialization.
|
||||
"""Telemetry stubs after passive-collection refactor.
|
||||
|
||||
Sets up OTLP exporters for traces, logs, and metrics when
|
||||
`OTEL_EXPORTER_OTLP_ENDPOINT` is set; otherwise no-op. httpx auto-instrumentation
|
||||
is wired so Ollama Cloud calls appear as spans. Service entrypoints call
|
||||
`setup_telemetry()` early and `shutdown_telemetry()` on exit.
|
||||
The application no longer pushes OTLP traces, logs, or metrics. Logs are
|
||||
written to stdout/stderr by ``core/logging.py`` and collected by the
|
||||
container runtime or a sidecar (Vector in the ``observer`` profile,
|
||||
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
|
||||
(no code-level parsing needed).
|
||||
|
||||
This module imports `opentelemetry` — only services in the `obs` group import it
|
||||
(see docs/ARCHITECTURE.md §4). mypy resolves it via the dev group.
|
||||
``setup_telemetry()`` and ``shutdown_telemetry()`` remain as safe no-op
|
||||
entrypoints so service ``__main__`` modules do not all have to change at
|
||||
once. ``get_tracer()`` and ``get_meter()`` return lightweight no-op objects
|
||||
so any remaining instrumentation calls do not raise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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
|
||||
from typing import Any
|
||||
|
||||
|
||||
def setup_telemetry(service_name: str | None = None) -> None:
|
||||
"""Initialize OTel tracing/logging/metrics. Safe to call when disabled."""
|
||||
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
|
||||
class _NoopSpan:
|
||||
"""Span-like object that ignores all operations."""
|
||||
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
||||
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
|
||||
def end(self, _timestamp: Any = None) -> None:
|
||||
return None
|
||||
|
||||
class StructlogOTLPLogHandler(LoggingHandler):
|
||||
"""OTEL handler that unwraps structlog event dicts.
|
||||
def set_attribute(self, _key: str, _value: Any) -> None:
|
||||
return None
|
||||
|
||||
``core/logging.py`` routes structlog through stdlib logging; the
|
||||
resulting ``LogRecord`` carries the event dictionary in
|
||||
``record.msg``. This handler extracts the ``event`` as the log body and
|
||||
promotes the remaining fields to OTEL attributes so OpenObserve can
|
||||
index them.
|
||||
"""
|
||||
def record_exception(self, _exception: BaseException, _attributes: Any = None) -> None:
|
||||
return None
|
||||
|
||||
_STRUCTLOG_INTERNAL_KEYS = frozenset({"_logger", "_name"})
|
||||
def __enter__(self) -> _NoopSpan:
|
||||
return self
|
||||
|
||||
def _translate(self, record: logging.LogRecord) -> Any:
|
||||
# Detect structlog-wrapped records produced by
|
||||
# ``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)
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
resource = Resource.create({"service.name": service_name or settings.otel_service_name})
|
||||
|
||||
# Traces. Exporters read OTEL_EXPORTER_OTLP_ENDPOINT and append the
|
||||
# signal-specific path (/v1/traces, /v1/logs, /v1/metrics) automatically.
|
||||
trace_provider = TracerProvider(resource=resource)
|
||||
trace_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
||||
set_tracer_provider(trace_provider)
|
||||
_trace_provider = trace_provider
|
||||
class _NoopTracer:
|
||||
"""Tracer-like object that returns no-op spans."""
|
||||
|
||||
# Logs (stdlib logging → OTLP). ``configure_logging`` routes structlog
|
||||
# through the stdlib logging tree; this handler unwraps the structured
|
||||
# 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
|
||||
def start_span(self, _name: str, _context: Any = None, _kind: Any = None) -> _NoopSpan:
|
||||
return _NoopSpan()
|
||||
|
||||
otel_log_handler = StructlogOTLPLogHandler(logger_provider=log_provider)
|
||||
otel_log_handler.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO))
|
||||
logging.getLogger().addHandler(otel_log_handler)
|
||||
def start_as_current_span(self, _name: str, *_args: Any, **_kwargs: Any) -> _NoopSpan:
|
||||
return _NoopSpan()
|
||||
|
||||
# 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:
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
class _NoopCounter:
|
||||
"""Counter-like object that ignores add() calls."""
|
||||
|
||||
HTTPXClientInstrumentor().instrument()
|
||||
except Exception as exc: # pragma: no cover - optional instrumentor
|
||||
log.warning("otel_httpx_instrument_failed", error=str(exc))
|
||||
def add(self, _amount: int | float, _attributes: Any = None) -> None:
|
||||
return None
|
||||
|
||||
log.info("otel_initialized", endpoint=endpoint)
|
||||
# Stdlib log so the OTel handler exports a verification record too.
|
||||
logging.getLogger("contract_check.telemetry").info(
|
||||
"OpenTelemetry initialized for %s at %s",
|
||||
service_name or settings.otel_service_name,
|
||||
endpoint,
|
||||
)
|
||||
|
||||
class _NoopHistogram:
|
||||
"""Histogram-like object that ignores record() calls."""
|
||||
|
||||
def record(self, _amount: int | float, _attributes: Any = None) -> None:
|
||||
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:
|
||||
"""Flush and shut down all OTel providers if initialized."""
|
||||
global _trace_provider, _log_provider, _meter_provider
|
||||
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
|
||||
"""No-op telemetry shutdown retained for backward compatibility."""
|
||||
return None
|
||||
|
||||
|
||||
def get_tracer(name: str | None = None) -> Any:
|
||||
"""Return a tracer. Returns a no-op-safe tracer when OTel is absent."""
|
||||
from opentelemetry import trace
|
||||
def get_tracer(_name: str | None = None) -> _NoopTracer:
|
||||
"""Return a no-op tracer.
|
||||
|
||||
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:
|
||||
"""Return a meter. Returns a no-op-safe meter when OTel is absent."""
|
||||
from opentelemetry import metrics
|
||||
def get_meter(_name: str | None = None) -> _NoopMeter:
|
||||
"""Return a no-op meter.
|
||||
|
||||
return metrics.get_meter(name or "contract_check")
|
||||
The application no longer uses OpenTelemetry; any existing instrument
|
||||
calls are silently ignored.
|
||||
"""
|
||||
return _METER
|
||||
|
|
|
|||
|
|
@ -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.metrics import start_metrics_server
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -25,7 +24,6 @@ async def main() -> None:
|
|||
)
|
||||
bind_context(service="worker-analyze", env=settings.env)
|
||||
init_sentry("worker-analyze")
|
||||
setup_telemetry("worker-analyze")
|
||||
|
||||
start_metrics_server(9102)
|
||||
|
||||
|
|
@ -55,7 +53,6 @@ async def main() -> None:
|
|||
)
|
||||
finally:
|
||||
await consumer.stop()
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -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.metrics import start_metrics_server
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -30,7 +29,6 @@ async def main() -> None:
|
|||
)
|
||||
bind_context(service="worker-billing", env=settings.env)
|
||||
init_sentry("worker-billing")
|
||||
setup_telemetry("worker-billing")
|
||||
|
||||
start_metrics_server(9105)
|
||||
|
||||
|
|
@ -64,7 +62,6 @@ async def main() -> None:
|
|||
if provider is not None:
|
||||
await provider.aclose()
|
||||
await dispose_engine()
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -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.metrics import start_metrics_server
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -25,7 +24,6 @@ async def main() -> None:
|
|||
)
|
||||
bind_context(service="worker-extract", env=settings.env)
|
||||
init_sentry("worker-extract")
|
||||
setup_telemetry("worker-extract")
|
||||
|
||||
start_metrics_server(9101)
|
||||
|
||||
|
|
@ -55,7 +53,6 @@ async def main() -> None:
|
|||
)
|
||||
finally:
|
||||
await consumer.stop()
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -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.metrics import start_metrics_server
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -25,7 +24,6 @@ async def main() -> None:
|
|||
)
|
||||
bind_context(service="worker-notify", env=settings.env)
|
||||
init_sentry("worker-notify")
|
||||
setup_telemetry("worker-notify")
|
||||
|
||||
start_metrics_server(9103)
|
||||
|
||||
|
|
@ -55,7 +53,6 @@ async def main() -> None:
|
|||
)
|
||||
finally:
|
||||
await consumer.stop()
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -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.metrics import start_metrics_server
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -25,7 +24,6 @@ async def main() -> None:
|
|||
)
|
||||
bind_context(service="worker-prescreen", env=settings.env)
|
||||
init_sentry("worker-prescreen")
|
||||
setup_telemetry("worker-prescreen")
|
||||
|
||||
start_metrics_server(9104)
|
||||
|
||||
|
|
@ -55,7 +53,6 @@ async def main() -> None:
|
|||
)
|
||||
finally:
|
||||
await consumer.stop()
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ WORKDIR /app
|
|||
COPY pyproject.toml uv.lock ./
|
||||
COPY README.md ./
|
||||
|
||||
# Install only the analyze group (core + db/mq/s3/obs + otel-httpx). No
|
||||
# tesseract/pymupdf — this image is the leanest LLM/I/O worker.
|
||||
# Install only the analyze group (core + db/mq/s3/obs). No tesseract/pymupdf
|
||||
# — this image is the leanest LLM/I/O worker.
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-default-groups --group analyze --no-install-project
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
OpenObserve receives structured attributes.
|
||||
The application no longer pushes OTLP. This module verifies that telemetry
|
||||
setup/shutdown are safe no-ops and that structured logging still reaches the
|
||||
configured root handler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -11,6 +12,7 @@ import logging
|
|||
import pytest
|
||||
|
||||
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)
|
||||
|
|
@ -22,82 +24,64 @@ def _reset_logging():
|
|||
root.handlers.clear()
|
||||
|
||||
|
||||
def test_structlog_otlp_handler_unwraps_event_dict(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A structlog-wrapped record is converted to body + attributes."""
|
||||
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")
|
||||
def test_setup_telemetry_is_safe_noop() -> None:
|
||||
"""setup_telemetry() runs without error and does not add an OTLP handler."""
|
||||
root = logging.getLogger()
|
||||
before = list(root.handlers)
|
||||
|
||||
setup_telemetry("test")
|
||||
try:
|
||||
otel_handler = next(
|
||||
h for h in root.handlers if type(h).__name__ == "StructlogOTLPLogHandler"
|
||||
)
|
||||
|
||||
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
|
||||
after = list(root.handlers)
|
||||
# No new handlers are attached because OTLP is no longer initialized.
|
||||
assert after == before
|
||||
finally:
|
||||
shutdown_telemetry()
|
||||
root.handlers = [h for h in root.handlers if type(h).__name__ != "StructlogOTLPLogHandler"]
|
||||
|
||||
|
||||
def test_structlog_logs_reach_root_handlers() -> None:
|
||||
"""After configure_logging, structlog events propagate to stdlib handlers."""
|
||||
def test_shutdown_telemetry_is_safe_noop() -> None:
|
||||
"""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()
|
||||
assert (
|
||||
any(type(h).__name__ in ("StreamHandler", "ProcessorFormatter") for h in root.handlers)
|
||||
or root.handlers
|
||||
)
|
||||
before = list(root.handlers)
|
||||
setup_telemetry("test")
|
||||
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.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
457
uv.lock
generated
|
|
@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "asyncpg"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "click"
|
||||
version = "8.5.0"
|
||||
|
|
@ -573,9 +470,6 @@ analyze = [
|
|||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "minio" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "sqlalchemy" },
|
||||
|
|
@ -589,11 +483,6 @@ api = [
|
|||
{ name = "fastapi" },
|
||||
{ name = "jinja2" },
|
||||
{ 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 = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
|
|
@ -606,9 +495,6 @@ api = [
|
|||
billing = [
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "sentry-sdk" },
|
||||
|
|
@ -642,11 +528,6 @@ dev = [
|
|||
{ name = "jinja2" },
|
||||
{ name = "mammoth" },
|
||||
{ name = "minio" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-asgi" },
|
||||
{ name = "opentelemetry-instrumentation-fastapi" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "prometheus-client" },
|
||||
|
|
@ -676,9 +557,6 @@ extract = [
|
|||
{ name = "chardet" },
|
||||
{ name = "mammoth" },
|
||||
{ name = "minio" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "pillow" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "pymupdf" },
|
||||
|
|
@ -696,16 +574,11 @@ notify = [
|
|||
{ name = "aiosmtplib" },
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
obs = [
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "sentry-sdk" },
|
||||
]
|
||||
|
|
@ -714,9 +587,6 @@ prescreen = [
|
|||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "minio" },
|
||||
{ name = "opentelemetry-exporter-otlp" },
|
||||
{ name = "opentelemetry-instrumentation-httpx" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "prometheus-client" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "sqlalchemy" },
|
||||
|
|
@ -740,9 +610,6 @@ analyze = [
|
|||
{ name = "alembic", specifier = ">=1.13" },
|
||||
{ name = "asyncpg", specifier = ">=0.29" },
|
||||
{ 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 = "sentry-sdk", specifier = ">=2" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0" },
|
||||
|
|
@ -756,11 +623,6 @@ api = [
|
|||
{ name = "fastapi", specifier = ">=0.110" },
|
||||
{ name = "jinja2", specifier = ">=3.1" },
|
||||
{ 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 = "pyjwt", extras = ["crypto"], specifier = ">=2.8" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.9" },
|
||||
|
|
@ -773,9 +635,6 @@ api = [
|
|||
billing = [
|
||||
{ name = "alembic", specifier = ">=1.13" },
|
||||
{ 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 = "pyjwt", specifier = ">=2.8" },
|
||||
{ name = "sentry-sdk", specifier = ">=2" },
|
||||
|
|
@ -809,11 +668,6 @@ dev = [
|
|||
{ name = "jinja2", specifier = ">=3.1" },
|
||||
{ name = "mammoth", specifier = ">=1.8" },
|
||||
{ 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 = "pre-commit", specifier = ">=4.6.2" },
|
||||
{ name = "prometheus-client", specifier = ">=0.20" },
|
||||
|
|
@ -843,9 +697,6 @@ extract = [
|
|||
{ name = "chardet", specifier = ">=5.2" },
|
||||
{ name = "mammoth", specifier = ">=1.8" },
|
||||
{ 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 = "prometheus-client", specifier = ">=0.20" },
|
||||
{ name = "pymupdf", specifier = ">=1.24" },
|
||||
|
|
@ -861,16 +712,11 @@ notify = [
|
|||
{ name = "aiosmtplib", specifier = ">=3.0" },
|
||||
{ name = "alembic", specifier = ">=1.13" },
|
||||
{ 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 = "sentry-sdk", specifier = ">=2" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0" },
|
||||
]
|
||||
obs = [
|
||||
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.24" },
|
||||
{ name = "opentelemetry-sdk", specifier = ">=1.24" },
|
||||
{ name = "prometheus-client", specifier = ">=0.20" },
|
||||
{ name = "sentry-sdk", specifier = ">=2" },
|
||||
]
|
||||
|
|
@ -879,9 +725,6 @@ prescreen = [
|
|||
{ name = "alembic", specifier = ">=1.13" },
|
||||
{ name = "asyncpg", specifier = ">=0.29" },
|
||||
{ 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 = "sentry-sdk", specifier = ">=2" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "greenlet"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "h11"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "packaging"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pyasn1"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "respx"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "yarl"
|
||||
version = "1.24.5"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue