diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..8da1800 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,54 @@ +name: Docs + +on: + push: + branches: [main] + paths: ["docs/**", "mkdocs.yml", "pyproject.toml", "uv.lock"] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install docs dependencies + run: uv sync --group docs + + - name: Build with mkdocs + run: uv run mkdocs build --strict + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 798061c..105d964 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ htmlcov/ # ruff .ruff_cache/ +# Docs +site/ + # Database *.db *.sqlite diff --git a/Makefile b/Makefile index f66f475..f5dc69c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install api consume mcp test lint lint-fix typecheck check migrate migrate-create clean +.PHONY: help install api consume mcp test lint lint-fix typecheck check migrate migrate-create docs docs-serve clean help: ## Show this help message @echo "Available commands:" @@ -38,6 +38,12 @@ migrate: ## Run database migrations migrate-create: ## Create new migration (usage: make migrate-create MSG="description") uv run alembic revision --autogenerate -m "$(MSG)" +docs: ## Build the documentation site + uv run mkdocs build + +docs-serve: ## Serve documentation locally (http://localhost:9000) + uv run mkdocs serve -a localhost:9000 + clean: ## Remove caches and build artifacts find . -type d -name "__pycache__" -exec rm -rf {} + find . -type d -name ".pytest_cache" -exec rm -rf {} + diff --git a/README.md b/README.md index ac360ae..a94c389 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # taskowl +[![Documentation](https://img.shields.io/badge/docs-github_pages-blue)](https://kalvadtech.github.io/taskowl/) + Modern Celery task monitoring with MCP integration. No UI, just data. ## Features @@ -9,13 +11,20 @@ Modern Celery task monitoring with MCP integration. No UI, just data. - **MCP-first**: Query and manage tasks, workers, and queues via the Model Context Protocol - **Event sourcing**: Append-only event log for a complete audit trail and state reconstruction - **Real-time monitoring**: Capture Celery events as they happen -- **Task actions**: Revoke, retry, and recover orphaned tasks -- **Worker management**: List, inspect, scale, and shut down workers -- **Alerts**: Slack-compatible webhook notifications on failures, slow tasks, and offline workers -- **Prometheus metrics**: Scrape task and worker telemetry via `/metrics` +- **Task actions**: Revoke, retry, recover orphaned tasks, and execute tasks by name +- **Worker management**: List, inspect, scale, restart, and shut down workers +- **Queue monitoring**: Per-queue message and consumer counts for any kombu broker +- **Workflow automations**: Declarative trigger → conditions → actions engine with + webhooks, retry orchestration, cooldowns, rate limits, and circuit breakers +- **Prometheus metrics**: Scrape task, worker, and automation telemetry via `/metrics` - **PostgreSQL backend**: Production-ready, async throughout - **Broker-agnostic**: RabbitMQ, LavinMQ, Redis, or any Celery/kombu broker +## Documentation + +The full documentation lives on [GitHub Pages](https://kalvadtech.github.io/taskowl/) — +setup, configuration, a complete usage guide, and troubleshooting. + ## Quick Start ### Prerequisites @@ -46,11 +55,9 @@ make consume # Celery event consumer make mcp # MCP server on :8001 ``` -## Configuring your Celery app +### Connect your Celery app -taskowl listens to Celery's **events** stream, which workers emit only if -enabled. Add this to your Celery application so taskowl can see your tasks and -workers: +taskowl listens to Celery's **events** stream, which workers emit only if enabled: ```python # celery_app.py @@ -58,34 +65,23 @@ from celery import Celery app = Celery("myapp", broker="amqp://guest:guest@localhost:5672//") -# Workers emit task/worker events (sent, received, started, succeeded, failed, ...) app.conf.worker_send_task_events = True - -# Emit a 'task-sent' event when a task is published app.conf.task_send_sent_event = True - -# How often workers send a heartbeat (default: 2s). Higher values -# increase the worker-offline detection delay. app.conf.worker_heartbeat_interval = 2 ``` -Alternatively, start your worker with the `-E` flag, which is equivalent to -`worker_send_task_events = True`: +Or start your worker with `-E`: ```bash celery -A myapp worker -E --loglevel=info ``` > **Note**: If events are not enabled, taskowl simply sees nothing — no tasks, -> no workers. Enabling events is the one integration required. +> no workers. -## Connecting MCP clients +### Connect an MCP client -The MCP server runs on `http://localhost:8001/mcp` (Streamable HTTP). - -### opencode - -Add a remote MCP server to your `opencode.json`: +The MCP server runs on `http://localhost:8001/mcp` (Streamable HTTP). For opencode: ```json { @@ -100,288 +96,6 @@ Add a remote MCP server to your `opencode.json`: } ``` -### Other MCP clients - -Point your MCP client at the Streamable HTTP endpoint `http://localhost:8001/mcp`. -If authentication is enabled (see below), send the taskowl API key as -`Authorization: Bearer ` with each request. - -## Available tools - -| Category | Tools | -|---|---| -| **Tasks** | `list_tasks`, `get_task`, `get_task_timeline`, `get_task_chain`, `get_task_summary`, `list_task_types`, `list_orphaned_tasks` | -| **Task actions** | `revoke_task`, `retry_task`, `execute_task` | -| **Workers** | `get_worker_status`, `list_workers`, `get_worker_stats`, `shutdown_worker`, `scale_worker_pool`, `restart_worker_pool`, `get_active_tasks`, `get_scheduled_tasks`, `get_reserved_tasks` | -| **Queues** | `list_queues` | -| **Automations** | `list_automations`, `create_automation`, `get_automation`, `update_automation`, `delete_automation`, `toggle_automation`, `get_automation_runs`, `get_automation_status` | - -**Total: 28 tools** - -`list_tasks` supports exact filters (`state`, `name`, `worker`, `since`), a partial -case-insensitive `search` on the task name, `offset` for pagination, and `sort_by` -(`timestamp` [default, newest-first], `name`, `state`, `worker`). - -### Automations - -Automations are declarative **trigger → conditions → actions** definitions that drive -workflow automation (a superset of the env-var alerts). They are managed via the -`/api/automations` endpoints and the `*_automation` MCP tools. - -Event triggers are evaluated by the consumer process: when an enabled automation's -`event_type` matches an incoming Celery event and all its `conditions` pass, its actions -fire. Every evaluation is recorded in the append-only `automation_runs` log (metadata only -— args, kwargs, results, and tracebacks are never stored), queryable via -`GET /api/automations/{id}/runs` and the `get_automation_runs` MCP tool. - -```bash -curl -X POST http://localhost:8000/api/automations \ - -H "Authorization: Bearer $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "alert-on-failure", - "trigger_type": "event", - "event_type": "task-failed", - "conditions": [{"field": "name", "op": "eq", "value": "payments.charge"}], - "actions": [{"type": "log"}] - }' -``` - -Trigger types: `event` (with `event_type`) or `periodic` (with `schedule_seconds`). -Conditions use `{field, op, value}` against event fields (including dotted paths) with ops -`eq/neq/gt/gte/lt/lte/contains/matches/in/exists`. - -Action types: -- `log` — write to the application log (`level`, `message`) -- `slack_webhook` — Slack-formatted webhook (`webhook_url` or `ALERT_WEBHOOK_URL`, `text`, `fields`) -- `webhook` — generic JSON webhook (`url`, `payload`) -- `retry_task` — retry the event's task (`task_id` defaults to the event `uuid`) -- `execute_task` — send a task by name (`name`, `args`, `kwargs`, `queue`, `countdown`, `eta`, `expires`, `priority`) -- `revoke_task` — revoke the event's task (`task_id` defaults to the event `uuid`, `terminate`) -- `check_workers_offline` — scan for stale/offline workers and alert (used by the - seeded `alert-worker-offline-sweep` periodic automation) - -Action params support `{event.field}` interpolation (e.g. `"task_id": "{event.uuid}"`). - -Safety knobs prevent alert/action storms: -- `cooldown_seconds` — after firing, wait at least this long before firing again -- `max_runs_per_window` + `window_seconds` — fire at most `max_runs_per_window` times per `window_seconds` -- `circuit_breaker` — `{"failure_threshold": N, "window_seconds": W}`; skips actions - (`"circuit_open"`) once the automation has fired N times within W seconds, and auto-closes - once the window rolls past - -Skipped evaluations (cooldown, rate limit, or circuit open) are still recorded in -`automation_runs` with a `details.skipped` reason (`"cooldown"` / `"rate_limited"` / -`"circuit_open"`), keeping storm suppression auditable. - -**Periodic triggers**: automations with `trigger_type: "periodic"` and `schedule_seconds` -run on a schedule (evaluated by the consumer's periodic loop, checked every -`AUTOMATION_CHECK_SECONDS`). Their conditions are evaluated against an empty event context, -so they are typically used for schedule-driven actions (e.g. a heartbeat webhook). - -## Examples - -Questions you can ask your AI assistant when the MCP server is connected: - -| Question | Tools used | -|---|---| -| "Show me failed tasks from the last hour" | `list_tasks` | -| "Which task types are running?" | `list_task_types` | -| "Which tasks are orphaned?" | `list_orphaned_tasks` | -| "Show me the timeline for task abc" | `get_task_timeline` | -| "What's the retry chain for task abc?" | `get_task_chain` | -| "What's the task success rate in the last 30 minutes?" | `get_task_summary` | -| "Which workers are online?" | `get_worker_status`, `list_workers` | -| "How many messages are in each queue?" | `list_queues` | -| "Shutdown worker celery@worker1" | `shutdown_worker` | -| "Restart the pool on celery@worker1" | `restart_worker_pool` | -| "What's scheduled to run next?" | `get_scheduled_tasks`, `get_reserved_tasks` | -| "Retry task abc" | `retry_task` | -| "Run myapp.tasks.process now" | `execute_task` | -| "Create an automation that alerts on payment failures" | `create_automation` | - -## Architecture - -``` -Celery workers ──events──▶ Broker ──▶ taskowl consumer ──▶ PostgreSQL - │ - REST API ◀───────────────────────────┘ - ▲ - │ HTTP - MCP server ──▶ LLM / MCP client -``` - -- **Consumer** (separate process) captures Celery events and appends them to - PostgreSQL (`task_events`, `worker_events`). -- **REST API** serves queries and actions over the event-sourcing tables. -- **MCP server** is a thin wrapper that calls the REST API for LLM access. - -## Configuration - -All configuration is via environment variables: - -| Variable | Description | Default | Required | -|----------|-------------|---------|----------| -| `DATABASE_URL` | PostgreSQL connection string | `postgresql+asyncpg://localhost:5432/taskowl` | Yes | -| `CELERY_BROKER_URL` | Celery broker URL (RabbitMQ, Redis, etc.) | `amqp://guest:guest@localhost:5672//` | Yes | -| `TASKOWL_HOST` | FastAPI server host | `0.0.0.0` | No | -| `TASKOWL_PORT` | FastAPI server port | `8000` | No | -| `MCP_HOST` | MCP server host | `0.0.0.0` | No | -| `MCP_PORT` | MCP server port | `8001` | No | -| `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `INFO` | No | -| `API_KEY` | API key for authentication (optional) | None (disabled) | No | -| `ORPHAN_GRACE_SECONDS` | Wait after task started before flagging as orphan | `60` | No | -| `WORKER_OFFLINE_TIMEOUT_SECONDS` | No heartbeat for this long means worker is offline | `30` | No | -| `ALERT_WEBHOOK_URL` | Slack webhook URL to post alerts to (disabled if unset) | None | No | -| `ALERT_ON_TASK_FAILED` | Enable task-failed alerts | `true` | No | -| `ALERT_ON_WORKER_OFFLINE` | Enable worker-offline alerts | `true` | No | -| `ALERT_SLOW_TASK_SECONDS` | Alert when a succeeded task exceeds this runtime | None | No | -| `ALERT_WORKER_CHECK_SECONDS` | Interval for the periodic stale-worker check | `30` | No | -| `AUTOMATION_CHECK_SECONDS` | Interval for the periodic automation evaluation loop | `5` | No | - -### Brokers - -taskowl works with any Celery/kombu broker via `CELERY_BROKER_URL`: - -```bash -export CELERY_BROKER_URL="amqp://guest:guest@localhost:5672//" # RabbitMQ / LavinMQ -export CELERY_BROKER_URL="redis://localhost:6379/0" # Redis -``` - -### Alerts / Webhooks - -> **Deprecated in favor of Automations.** The `ALERT_*` env vars below are -> legacy: on consumer startup they seed the equivalent built-in automations -> (`alert-task-failed`, `alert-slow-task`, `alert-worker-offline`, -> `alert-worker-offline-sweep`), which are then managed like any other -> automation via the API/MCP. Prefer defining automations directly. - -Set `ALERT_WEBHOOK_URL` to a Slack incoming webhook to receive notifications on -task failures, offline workers, and slow tasks. Alerting is **off by default**. - -```bash -export ALERT_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" -``` - -Conditions: - -- `ALERT_ON_TASK_FAILED=true` (default) — notify when a task fails -- `ALERT_ON_WORKER_OFFLINE=true` (default) — notify when a worker goes offline - (via a `worker-offline` event or a stale heartbeat detected every - `ALERT_WORKER_CHECK_SECONDS`) -- `ALERT_SLOW_TASK_SECONDS=30` — notify when a succeeded task exceeds 30s - -Payloads are Slack-formatted and contain task metadata only (name, task ID, -worker, error, runtime) — args, kwargs, and results are never sent. - -### Prometheus Metrics - -Scrape task and worker telemetry from the API server: - -```bash -curl http://localhost:8000/metrics -``` - -```yaml -scrape_configs: - - job_name: taskowl - metrics_path: /metrics - scrape_interval: 15s - static_configs: - - targets: ["localhost:8000"] -``` - -| Metric | Type | Labels | -|--------|------|--------| -| `taskowl_task_events_total` | Counter | `event_type`, `task_name`, `worker` | -| `taskowl_task_execution_duration_seconds` | Histogram | `task_name` | -| `taskowl_worker_status` | Gauge (1 = online, 0 = offline) | `worker` | -| `taskowl_worker_active_tasks` | Gauge | `worker` | -| `taskowl_worker_processed_total` | Counter | `worker` | -| `taskowl_automation_fired_total` | Counter (automation runs that fired actions) | `automation_id`, `trigger` | -| `taskowl_automation_skipped_total` | Counter (runs skipped by a safety mechanism) | `automation_id`, `reason` | - -> **Security**: `/metrics` is intentionally unauthenticated so Prometheus can -> scrape it without the taskowl API key. Only expose it to trusted networks or -> behind a reverse proxy. - -## Authentication - -Optional API key authentication protects the REST API and MCP server. Set -`API_KEY` to enable it; all requests must then include -`Authorization: Bearer `. - -```bash -export API_KEY="your-secret-key-here" -curl -H "Authorization: Bearer your-secret-key-here" http://localhost:8000/api/tasks -``` - -When authentication is disabled, all endpoints are open. `/health`, `/`, and -`/metrics` remain open regardless. - -## REST API - -The API server (port 8000) exposes a REST API for tasks, workers, orphans, -retries, and metrics. Interactive docs are available at: - -- Swagger UI: `http://localhost:8000/docs` -- ReDoc: `http://localhost:8000/redoc` -- Raw OpenAPI schema: `http://localhost:8000/openapi.json` - -| Area | Endpoints | -|------|-----------| -| **Tasks** | `GET /api/tasks`, `GET /api/tasks/{id}`, `GET /api/tasks/{id}/timeline`, `GET /api/tasks/{id}/chain`, `GET /api/tasks/summary`, `GET /api/tasks/types`, `GET /api/tasks/orphaned` | -| **Task actions** | `POST /api/tasks/{id}/revoke`, `POST /api/tasks/{id}/retry`, `POST /api/tasks/execute` | -| **Workers** | `GET /api/workers`, `GET /api/workers/list`, `GET /api/workers/{name}/stats`, `GET /api/workers/active-tasks`, `GET /api/workers/scheduled`, `GET /api/workers/reserved` | -| **Worker actions** | `POST /api/workers/{name}/shutdown`, `POST /api/workers/{name}/scale`, `POST /api/workers/{name}/restart` | -| **Queues** | `GET /api/queues` | -| **Automations** | `GET /api/automations`, `POST /api/automations`, `GET /api/automations/{id}`, `PUT /api/automations/{id}`, `DELETE /api/automations/{id}`, `POST /api/automations/{id}/toggle`, `GET /api/automations/{id}/runs`, `GET /api/automations/{id}/status` | -| **Ops** | `GET /health`, `GET /metrics` | - -The `/openapi.json` schema is the authoritative reference — this README lists -only endpoint groups. - -## Troubleshooting - -### Worker not appearing / no events in the database - -1. Verify workers emit events — start with `-E` or set `worker_send_task_events`. -2. Check the consumer connected: - ```bash - make consume 2>&1 | grep "Connected to Celery broker" - ``` -3. Verify events reach the broker: - ```bash - celery -A your_app events --dump - ``` -4. Check event counts in the database: - ```sql - SELECT COUNT(*) FROM task_events; - SELECT COUNT(*) FROM worker_events; - ``` - -### Database connection errors - -- `pg_isready` to confirm PostgreSQL is up. -- Check `DATABASE_URL` format: `postgresql+asyncpg://user:pass@host:port/dbname`. -- Verify the role has access to the database. - -### Broker connection errors - -- `rabbitmqctl status` (or your broker's health check) to confirm it's running. -- Check `CELERY_BROKER_URL` format and credentials. - -### Port already in use - -- API: `lsof -i :8000` / MCP: `lsof -i :8001` -- Kill the offending process: `kill $(lsof -t -i :8001)` - -### Getting help - -Open an issue with the error message, environment details, steps to reproduce, -and relevant (sanitized) logs: -https://github.com/KalvadTech/taskowl/issues - ## Contributing See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, code style, @@ -394,4 +108,4 @@ MIT — see [LICENSE](LICENSE) for details. ## Acknowledgments - [Flower](https://github.com/mher/flower) — the original Celery monitor -- [Kanchi](https://github.com/getkanchi/kanchi) — modern Celery monitoring inspiration +- [Kanchi](https://github.com/getkanchi/kanchi) — modern Celery monitoring inspiration \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..787a4ae --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,76 @@ +# Contributing + +Thank you for your interest in contributing to taskowl! + +## Development setup + +See the [Installation](setup/installation.md) guide to get a local environment +running, then continue here. + +## Development workflow + +1. **Create a feature branch**: + + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** — follow the existing code style, add tests, update docs. + +3. **Run quality checks**: + + ```bash + make check # Runs lint, typecheck, and tests + ``` + +4. **Commit and push**, then open a pull request. + +## Code style + +- **Formatter**: ruff (line length: 100) +- **Type checker**: ty (strict mode) +- **Import sorting**: ruff (isort) +- **Python version**: 3.14+ + +Guidelines: + +- **Type annotations**: All functions must have type hints. +- **Async/await**: Use async for I/O operations. +- **Error handling**: Be explicit about error cases — return `{"error": ...}` + dicts from core functions, surface them as HTTP 4xx/5xx in the API layer. +- **Documentation**: Document public APIs with docstrings. + +## Testing + +```bash +# Run all tests +make test + +# Run a specific test file +uv run pytest tests/test_queries.py -v +``` + +Tests live in `tests/`, named `test_.py`, using fixtures from +`conftest.py`. + +## Docs + +The site is built with MkDocs + Material. Edit pages under `docs/`, then verify +locally: + +```bash +make docs # build the site +make docs-serve # preview at http://localhost:9000 +``` + +## Pull requests + +Before submitting: all tests pass (`make check`), code is formatted +(`uv run ruff format .`), no lint errors, docs updated if needed, and tests added +for new functionality. At least one approval is required; merges are squash +merged to `main`. + +## License + +By contributing, you agree that your contributions will be licensed under the +MIT License. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9c2f0b3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +# taskowl + +Modern Celery task monitoring with MCP integration. No UI, just data. + +taskowl watches your Celery cluster's **event stream**, stores every event in +PostgreSQL as an append-only audit log, and exposes that data — plus task, +worker, and queue operations — through a REST API and a set of MCP tools for +LLM-driven monitoring and management. + +## Features + +- **MCP-first**: Query and manage tasks, workers, and queues via the Model Context Protocol +- **Event sourcing**: Append-only event log for a complete audit trail and state reconstruction +- **Real-time monitoring**: Capture Celery events as they happen +- **Task actions**: Revoke, retry, recover orphaned tasks, and execute tasks by name +- **Worker management**: List, inspect, scale, restart, and shut down workers +- **Queue monitoring**: Per-queue message and consumer counts for any kombu broker +- **Workflow automations**: Declarative trigger → conditions → actions engine with + webhooks, retry orchestration, cooldowns, rate limits, and circuit breakers +- **Prometheus metrics**: Scrape task, worker, and automation telemetry via `/metrics` +- **PostgreSQL backend**: Production-ready, async throughout +- **Broker-agnostic**: RabbitMQ, LavinMQ, Redis, or any Celery/kombu broker + +## Architecture + +``` +Celery workers ──events──▶ Broker ──▶ taskowl consumer ──▶ PostgreSQL + │ + REST API ◀───────────────────────────┘ + ▲ + │ HTTP + MCP server ──▶ LLM / MCP client +``` + +- **Consumer** (separate process) captures Celery events and appends them to + PostgreSQL (`task_events`, `worker_events`, `automation_runs`). +- **REST API** serves queries and actions over the event-sourcing tables. +- **MCP server** is a thin wrapper that calls the REST API for LLM access. + +## Get started + +Jump into the [Installation](setup/installation.md) guide to run taskowl, or head +straight to the [Usage Guide](usage/index.md) for the MCP tools and REST API. + +!!! note + taskowl only sees what your Celery workers emit. Make sure + [events are enabled](usage/celery-app.md) — otherwise taskowl sees nothing. \ No newline at end of file diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..d0dadf1 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/setup/configuration.md b/docs/setup/configuration.md new file mode 100644 index 0000000..aec2b8d --- /dev/null +++ b/docs/setup/configuration.md @@ -0,0 +1,45 @@ +# Configuration + +All configuration is via environment variables. + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `DATABASE_URL` | PostgreSQL connection string | `postgresql+asyncpg://localhost:5432/taskowl` | Yes | +| `CELERY_BROKER_URL` | Celery broker URL (RabbitMQ, Redis, etc.) | `amqp://guest:guest@localhost:5672//` | Yes | +| `TASKOWL_HOST` | FastAPI server host | `0.0.0.0` | No | +| `TASKOWL_PORT` | FastAPI server port | `8000` | No | +| `MCP_HOST` | MCP server host | `0.0.0.0` | No | +| `MCP_PORT` | MCP server port | `8001` | No | +| `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `INFO` | No | +| `API_KEY` | API key for authentication (optional) | None (disabled) | No | +| `ORPHAN_GRACE_SECONDS` | Wait after task started before flagging as orphan | `60` | No | +| `WORKER_OFFLINE_TIMEOUT_SECONDS` | No heartbeat for this long means worker is offline | `30` | No | +| `ALERT_WEBHOOK_URL` | Slack webhook URL to post alerts to (disabled if unset) | None | No | +| `ALERT_ON_TASK_FAILED` | Enable task-failed alerts (legacy; see Automations) | `true` | No | +| `ALERT_ON_WORKER_OFFLINE` | Enable worker-offline alerts (legacy; see Automations) | `true` | No | +| `ALERT_SLOW_TASK_SECONDS` | Alert when a succeeded task exceeds this runtime | None | No | +| `ALERT_WORKER_CHECK_SECONDS` | Interval for the periodic stale-worker check | `30` | No | +| `AUTOMATION_CHECK_SECONDS` | Interval for the periodic automation evaluation loop | `5` | No | + +## Brokers + +taskowl works with any Celery/kombu broker via `CELERY_BROKER_URL`: + +```bash +export CELERY_BROKER_URL="amqp://guest:guest@localhost:5672//" # RabbitMQ / LavinMQ +export CELERY_BROKER_URL="redis://localhost:6379/0" # Redis +``` + +## Authentication + +Optional API key authentication protects the REST API and MCP server. Set +`API_KEY` to enable it; all requests must then include +`Authorization: Bearer `. + +```bash +export API_KEY="your-secret-key-here" +curl -H "Authorization: Bearer your-secret-key-here" http://localhost:8000/api/tasks +``` + +When authentication is disabled, all endpoints are open. `/health`, `/`, and +`/metrics` remain open regardless. \ No newline at end of file diff --git a/docs/setup/index.md b/docs/setup/index.md new file mode 100644 index 0000000..967a51a --- /dev/null +++ b/docs/setup/index.md @@ -0,0 +1,11 @@ +# Setup + +taskowl runs as three separate processes that share a PostgreSQL database and a +Celery broker: + +- **API** — FastAPI REST server (default port `8000`) +- **Consumer** — captures Celery events from the broker and stores them +- **MCP** — MCP server exposing the tools to LLMs (default port `8001`) + +Follow the [Installation](installation.md) guide to get started, then read +[Configuration](configuration.md) for the full list of environment variables. \ No newline at end of file diff --git a/docs/setup/installation.md b/docs/setup/installation.md new file mode 100644 index 0000000..ab7e8b2 --- /dev/null +++ b/docs/setup/installation.md @@ -0,0 +1,61 @@ +# Installation + +## Prerequisites + +- Python 3.14+ +- PostgreSQL 14+ +- A Celery broker (RabbitMQ, LavinMQ, Redis, ...) +- [uv](https://github.com/astral-sh/uv) + +## Install and migrate + +```bash +git clone https://github.com/KalvadTech/taskowl.git +cd taskowl +make install + +export DATABASE_URL="postgresql+asyncpg://user:pass@localhost:5432/taskowl" +export CELERY_BROKER_URL="amqp://guest:guest@localhost:5672//" + +make migrate +``` + +## Run the three processes + +Open three terminals: + +```bash +make api # REST API on :8000 +make consume # Celery event consumer +make mcp # MCP server on :8001 +``` + +## Docker Compose + +taskowl ships a `docker-compose.yml` that runs the API, consumer, and MCP +server alongside PostgreSQL and RabbitMQ: + +```bash +docker compose up --build +``` + +The API is on `http://localhost:8000`, the MCP server on +`http://localhost:8001/mcp`. + +## Verify it's working + +1. Confirm the consumer connected to the broker: + + ```bash + make consume 2>&1 | grep "Connected to Celery broker" + ``` + +2. Check the API health endpoint: + + ```bash + curl http://localhost:8000/health + # {"status":"ok"} + ``` + +3. Make sure your [Celery app emits events](../usage/celery-app.md) — without them + taskowl sees nothing. \ No newline at end of file diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..be2170c --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,45 @@ +# Troubleshooting + +## Worker not appearing / no events in the database + +1. Verify workers emit events — start with `-E` or set `worker_send_task_events`. +2. Check the consumer connected: + + ```bash + make consume 2>&1 | grep "Connected to Celery broker" + ``` + +3. Verify events reach the broker: + + ```bash + celery -A your_app events --dump + ``` + +4. Check event counts in the database: + + ```sql + SELECT COUNT(*) FROM task_events; + SELECT COUNT(*) FROM worker_events; + ``` + +## Database connection errors + +- `pg_isready` to confirm PostgreSQL is up. +- Check `DATABASE_URL` format: `postgresql+asyncpg://user:pass@host:port/dbname`. +- Verify the role has access to the database. + +## Broker connection errors + +- `rabbitmqctl status` (or your broker's health check) to confirm it's running. +- Check `CELERY_BROKER_URL` format and credentials. + +## Port already in use + +- API: `lsof -i :8000` / MCP: `lsof -i :8001` +- Kill the offending process: `kill $(lsof -t -i :8001)` + +## Getting help + +Open an issue with the error message, environment details, steps to reproduce, +and relevant (sanitized) logs: +https://github.com/KalvadTech/taskowl/issues \ No newline at end of file diff --git a/docs/usage/automations.md b/docs/usage/automations.md new file mode 100644 index 0000000..d8b460a --- /dev/null +++ b/docs/usage/automations.md @@ -0,0 +1,173 @@ +# Automations + +Automations are declarative **trigger → conditions → actions** definitions that +drive workflow automation — a superset of the original env-var alerts. They are +evaluated by the consumer process, managed via the `/api/automations` endpoints, +and exposed as the `*_automation` MCP tools. + +## How evaluation works + +When the consumer receives an event, it loads every **enabled** event automation +whose `event_type` matches, evaluates the automation's `conditions` against the +event, and — if they all pass — fires its `actions`. Every evaluation is +recorded in the append-only `automation_runs` log (metadata only; args, kwargs, +results, and tracebacks are never stored). + +Periodic automations run on a schedule instead, evaluated by the consumer's +periodic loop (`AUTOMATION_CHECK_SECONDS`, default 5). + +## Anatomy of an automation + +```json +{ + "name": "alert-on-failure", + "enabled": true, + "trigger_type": "event", + "event_type": "task-failed", + "conditions": [{"field": "name", "op": "eq", "value": "payments.charge"}], + "actions": [{"type": "log"}], + "cooldown_seconds": null, + "max_runs_per_window": null, + "window_seconds": null, + "circuit_breaker": null +} +``` + +## Creating an automation + +```bash +curl -X POST http://localhost:8000/api/automations \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "alert-on-failure", + "trigger_type": "event", + "event_type": "task-failed", + "conditions": [{"field": "name", "op": "eq", "value": "payments.charge"}], + "actions": [{"type": "slack_webhook", "webhook_url": "https://hooks.slack.com/..."}] + }' +``` + +## Triggers + +| `trigger_type` | Required fields | Fires on | +|---|---|---| +| `event` | `event_type` | A matching Celery event (`task-failed`, `task-succeeded`, `worker-offline`, ...) | +| `periodic` | `schedule_seconds` | A schedule; conditions evaluate against an empty event context | + +## Conditions + +Conditions are `{field, op, value}` triples evaluated against the event. Field +paths support dots (e.g. `request.id`). All conditions must pass. + +| Op | Behavior | +|---|---| +| `eq` | Field equals value | +| `neq` | Field does not equal value | +| `gt` / `gte` | Field greater than / greater-or-equal (numeric) | +| `lt` / `lte` | Field less than / less-or-equal (numeric) | +| `contains` | String contains value, or list/dict contains value | +| `matches` | Field (stringified) matches a regex | +| `in` | Field is in a list | +| `exists` | Field is present (non-null) | + +Example — only alert for retried failures of one task on the high queue: + +```json +{ + "conditions": [ + {"field": "name", "op": "eq", "value": "payments.charge"}, + {"field": "retries", "op": "gte", "value": 1}, + {"field": "queue", "op": "in", "value": ["high", "critical"]} + ] +} +``` + +## Actions + +| Type | Parameters | +|---|---| +| `log` | `level`, `message` | +| `slack_webhook` | `webhook_url` (or `ALERT_WEBHOOK_URL`), `text`, `fields` (dict of `{label: value}`) | +| `webhook` | `url`, `payload` (JSON) | +| `retry_task` | `task_id` (defaults to event `uuid`) | +| `execute_task` | `name`, `args`, `kwargs`, `queue`, `countdown`, `eta`, `expires`, `priority` | +| `revoke_task` | `task_id` (defaults to event `uuid`), `terminate` | +| `check_workers_offline` | `webhook_url` — scan for stale workers and alert | + +Action params support **`{event.field}` interpolation** — the value is resolved +from the triggering event at fire time: + +```json +{ + "type": "webhook", + "url": "https://example.com/hooks", + "payload": {"task_id": "{event.uuid}", "task_name": "{event.name}"} +} +``` + +## Safety knobs + +Prevent alert/action storms: + +- **`cooldown_seconds`** — after firing, wait at least this long before firing again. +- **`max_runs_per_window` + `window_seconds`** — fire at most `max_runs_per_window` + times per `window_seconds`. +- **`circuit_breaker`** — `{"failure_threshold": N, "window_seconds": W}`; skip + actions (`circuit_open`) once the automation has fired N times within W + seconds, and auto-close once the window rolls past. + +Skipped evaluations are still recorded in `automation_runs` with a +`details.skipped` reason: `"cooldown"`, `"rate_limited"`, or `"circuit_open"`. + +## Observability + +- **Run history**: `GET /api/automations/{id}/runs` / `get_automation_runs`. +- **Status + circuit state**: `GET /api/automations/{id}/status` / + `get_automation_status` returns the automation, its latest run, and whether + the circuit is `open` or `closed`. +- **Prometheus**: `taskowl_automation_fired_total` and + `taskowl_automation_skipped_total` counters. + +## Worked example: retry with circuit breaker + +1. Retry `payments.charge` on failure, but never more than twice per hour and + pause entirely if it fails 3 times within 5 minutes: + +```json +{ + "name": "retry-payments", + "trigger_type": "event", + "event_type": "task-failed", + "conditions": [{"field": "name", "op": "eq", "value": "payments.charge"}], + "actions": [{"type": "execute_task", "name": "payments.charge", "countdown": 30}], + "max_runs_per_window": 2, + "window_seconds": 3600, + "circuit_breaker": {"failure_threshold": 3, "window_seconds": 300} +} +``` + +2. Tell the team when it finally trips the breaker: + +```json +{ + "name": "payments-breaker-alert", + "trigger_type": "event", + "event_type": "task-failed", + "conditions": [{"field": "name", "op": "eq", "value": "payments.charge"}], + "actions": [{ + "type": "slack_webhook", + "webhook_url": "https://hooks.slack.com/...", + "text": "payments.charge is failing repeatedly", + "fields": {"Task": "{event.name}", "Error": "{event.exception}"} + }], + "cooldown_seconds": 300 +} +``` + +## Legacy env-var alerts + +The `ALERT_*` env vars are **deprecated**. On consumer startup they seed the +equivalent built-in automations (`alert-task-failed`, `alert-slow-task`, +`alert-worker-offline`, `alert-worker-offline-sweep`), which are then managed +like any other automation. Prefer defining automations directly. \ No newline at end of file diff --git a/docs/usage/celery-app.md b/docs/usage/celery-app.md new file mode 100644 index 0000000..3d201b6 --- /dev/null +++ b/docs/usage/celery-app.md @@ -0,0 +1,33 @@ +# Configuring your Celery app + +taskowl listens to Celery's **events** stream, which workers emit only if +enabled. Add this to your Celery application so taskowl can see your tasks and +workers: + +```python +# celery_app.py +from celery import Celery + +app = Celery("myapp", broker="amqp://guest:guest@localhost:5672//") + +# Workers emit task/worker events (sent, received, started, succeeded, failed, ...) +app.conf.worker_send_task_events = True + +# Emit a 'task-sent' event when a task is published +app.conf.task_send_sent_event = True + +# How often workers send a heartbeat (default: 2s). Higher values +# increase the worker-offline detection delay. +app.conf.worker_heartbeat_interval = 2 +``` + +Alternatively, start your worker with the `-E` flag, which is equivalent to +`worker_send_task_events = True`: + +```bash +celery -A myapp worker -E --loglevel=info +``` + +!!! note + If events are not enabled, taskowl simply sees nothing — no tasks, no + workers. Enabling events is the one integration required. \ No newline at end of file diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 0000000..422f373 --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,49 @@ +# Usage Guide + +This guide walks through everything you can do with taskowl: querying tasks, +managing workers, inspecting queues, and building workflow automations. + +## Available MCP tools + +| Category | Tools | +|---|---| +| **Tasks** | `list_tasks`, `get_task`, `get_task_timeline`, `get_task_chain`, `get_task_summary`, `list_task_types`, `list_orphaned_tasks` | +| **Task actions** | `revoke_task`, `retry_task`, `execute_task` | +| **Workers** | `get_worker_status`, `list_workers`, `get_worker_stats`, `shutdown_worker`, `scale_worker_pool`, `restart_worker_pool`, `get_active_tasks`, `get_scheduled_tasks`, `get_reserved_tasks` | +| **Queues** | `list_queues` | +| **Automations** | `list_automations`, `create_automation`, `get_automation`, `update_automation`, `delete_automation`, `toggle_automation`, `get_automation_runs`, `get_automation_status` | + +**Total: 28 tools** — every tool thin-wraps a REST API endpoint, so anything you +can do via MCP you can also do with `curl` against the API. + +## Example questions + +Once your MCP client is connected, you can ask: + +| Question | Tools used | +|---|---| +| "Show me failed tasks from the last hour" | `list_tasks` | +| "Which task types are running?" | `list_task_types` | +| "Which tasks are orphaned?" | `list_orphaned_tasks` | +| "Show me the timeline for task abc" | `get_task_timeline` | +| "What's the retry chain for task abc?" | `get_task_chain` | +| "What's the task success rate in the last 30 minutes?" | `get_task_summary` | +| "Which workers are online?" | `get_worker_status`, `list_workers` | +| "How many messages are in each queue?" | `list_queues` | +| "Shutdown worker celery@worker1" | `shutdown_worker` | +| "Restart the pool on celery@worker1" | `restart_worker_pool` | +| "What's scheduled to run next?" | `get_scheduled_tasks`, `get_reserved_tasks` | +| "Retry task abc" | `retry_task` | +| "Run myapp.tasks.process now" | `execute_task` | +| "Create an automation that alerts on payment failures" | `create_automation` | + +## Structure + +- [Configuring Celery](celery-app.md) — make taskowl see your tasks and workers +- [MCP Clients](mcp-clients.md) — connect opencode or any MCP client +- [Tasks](tasks.md) — query tasks, filters, timelines, chains, summaries, orphans +- [Task Actions](task-actions.md) — revoke, retry, execute +- [Workers](workers.md) — status, stats, scale, restart, shutdown, scheduled/reserved +- [Queues](queues.md) — broker queue lengths and consumer counts +- [Automations](automations.md) — declarative trigger → conditions → actions engine +- [Metrics](metrics.md) — Prometheus telemetry \ No newline at end of file diff --git a/docs/usage/mcp-clients.md b/docs/usage/mcp-clients.md new file mode 100644 index 0000000..5e85745 --- /dev/null +++ b/docs/usage/mcp-clients.md @@ -0,0 +1,26 @@ +# Connecting MCP clients + +The MCP server runs on `http://localhost:8001/mcp` (Streamable HTTP). + +## opencode + +Add a remote MCP server to your `opencode.json`: + +```json +{ + "mcp": { + "taskowl": { + "type": "remote", + "url": "http://localhost:8001/mcp", + "enabled": true, + "oauth": false + } + } +} +``` + +## Other MCP clients + +Point your MCP client at the Streamable HTTP endpoint `http://localhost:8001/mcp`. +If authentication is enabled, send the taskowl API key as +`Authorization: Bearer ` with each request. \ No newline at end of file diff --git a/docs/usage/metrics.md b/docs/usage/metrics.md new file mode 100644 index 0000000..84bf845 --- /dev/null +++ b/docs/usage/metrics.md @@ -0,0 +1,31 @@ +# Metrics + +taskowl exposes Prometheus metrics at `/metrics` on the API server. + +```bash +curl http://localhost:8000/metrics +``` + +```yaml +scrape_configs: + - job_name: taskowl + metrics_path: /metrics + scrape_interval: 15s + static_configs: + - targets: ["localhost:8000"] +``` + +| Metric | Type | Labels | +|--------|------|--------| +| `taskowl_task_events_total` | Counter | `event_type`, `task_name`, `worker` | +| `taskowl_task_execution_duration_seconds` | Histogram | `task_name` | +| `taskowl_worker_status` | Gauge (1 = online, 0 = offline) | `worker` | +| `taskowl_worker_active_tasks` | Gauge | `worker` | +| `taskowl_worker_processed_total` | Counter | `worker` | +| `taskowl_automation_fired_total` | Counter (automation runs that fired actions) | `automation_id`, `trigger` | +| `taskowl_automation_skipped_total` | Counter (runs skipped by a safety mechanism) | `automation_id`, `reason` | + +!!! warning + `/metrics` is intentionally unauthenticated so Prometheus can scrape it + without the taskowl API key. Only expose it to trusted networks or behind a + reverse proxy. \ No newline at end of file diff --git a/docs/usage/queues.md b/docs/usage/queues.md new file mode 100644 index 0000000..7f5d739 --- /dev/null +++ b/docs/usage/queues.md @@ -0,0 +1,28 @@ +# Queues + +Inspect the Celery broker's queues. Works with any kombu transport (RabbitMQ, +LavinMQ, Redis) via `CELERY_BROKER_URL`. + +## list_queues + +Report per-queue message and consumer counts for the declared queues, plus a +`total_messages` aggregate. Queues are ordered by message count descending. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/queues +``` + +Example response: + +```json +{ + "queues": [ + {"name": "default", "messages": 42, "consumers": 2}, + {"name": "high", "messages": 5, "consumers": 1} + ], + "total_messages": 47 +} +``` + +Uses kombu's passive `queue_declare`, so no broker state is modified. If the +broker is unreachable, the endpoint returns a `502`. \ No newline at end of file diff --git a/docs/usage/task-actions.md b/docs/usage/task-actions.md new file mode 100644 index 0000000..8bede63 --- /dev/null +++ b/docs/usage/task-actions.md @@ -0,0 +1,56 @@ +# Task Actions + +Write operations on tasks. All require authentication (`API_KEY`) and follow +the pattern `actions.py → main.py → mcp/tools.py`. + +## revoke_task + +Revoke (cancel) a task. Optionally terminate it if it is currently running. + +| Parameter | Description | +|---|---| +| `task_id` | UUID of the task to revoke | +| `terminate` | If `true`, terminate the task if it's currently running | + +```bash +curl -X POST -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/tasks//revoke?terminate=true" +``` + +## retry_task + +Retry a failed, revoked, or orphaned task by creating a new task with the same +parameters. The retry chain is preserved via `root_id`/`parent_id`, so +`get_task_chain` shows the full retry family. + +```bash +curl -X POST -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks//retry +``` + +## execute_task + +Execute a task by name — the Flower "send-task" equivalent. You provide the task +name, args, kwargs, and scheduling options directly; no prior task is needed. + +| Parameter | Description | +|---|---| +| `name` | Task name (e.g. `myapp.tasks.process`) | +| `args` | Positional arguments (JSON array) | +| `kwargs` | Keyword arguments (JSON object) | +| `queue` | Queue to send to (defaults to app default) | +| `countdown` | Seconds to wait before the task runs | +| `eta` | ISO 8601 datetime before which the task should not run | +| `expires` | ISO 8601 datetime after which the task expires | +| `priority` | Queue priority (0-9, broker-dependent) | + +```bash +curl -X POST -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + http://localhost:8000/api/tasks/execute \ + -d '{"name": "myapp.tasks.process", "kwargs": {"id": 42}, "queue": "high"}' +``` + +!!! note + `execute_task` does not check task registration — the worker surfaces a + `NotRegistered` error if the name is unknown. \ No newline at end of file diff --git a/docs/usage/tasks.md b/docs/usage/tasks.md new file mode 100644 index 0000000..c13097e --- /dev/null +++ b/docs/usage/tasks.md @@ -0,0 +1,89 @@ +# Tasks + +Query and inspect Celery tasks reconstructed from the append-only event log. + +## list_tasks + +List tasks with optional filters. + +| Parameter | Description | +|---|---| +| `state` | Filter by state (`received`, `started`, `succeeded`, `failed`, `retried`, `revoked`) | +| `name` | Filter by **exact** task name | +| `worker` | Filter by worker hostname | +| `since` | Only tasks created after this datetime (ISO 8601) | +| `search` | Partial, case-insensitive match on the task name | +| `offset` | Number of tasks to skip (pagination) | +| `sort_by` | `timestamp` (default, newest-first), `name`, `state`, `worker` | +| `limit` | Max number of tasks to return (default: 100) | + +```bash +# Failed tasks from the last hour, newest first +curl -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/tasks?state=failed&since=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S)" + +# Search for a task type by partial name +curl -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/tasks?search=payments" +``` + +## get_task + +Get detailed information about a specific task, including all of its events and +its current state, worker, orphan status, and result. + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks/ +``` + +## get_task_timeline + +Get all events for a task in chronological order — the full execution flow +(sent, received, started, succeeded/failed, ...). + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks//timeline +``` + +## get_task_chain + +Get the full retry chain for a task: the original task plus every retry, +ordered chronologically, with parent linkage. + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks//chain +``` + +## get_task_summary + +Aggregate task statistics over a time window: total tasks, breakdown by state, +and average runtime of succeeded tasks. + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/tasks/summary?hours=1" +``` + +## list_task_types + +List distinct task names (types) with a count of how many tasks each has run. + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks/types +``` + +## list_orphaned_tasks + +List tasks stuck in `STARTED` whose worker went offline (crashed / network +drop) before sending a completion event. A task is orphaned when it has been in +`STARTED` longer than `ORPHAN_GRACE_SECONDS` and its worker is offline per +`WORKER_OFFLINE_TIMEOUT_SECONDS`. + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/tasks/orphaned +``` \ No newline at end of file diff --git a/docs/usage/workers.md b/docs/usage/workers.md new file mode 100644 index 0000000..e31ed0b --- /dev/null +++ b/docs/usage/workers.md @@ -0,0 +1,91 @@ +# Workers + +Inspect and manage Celery workers. Read operations use `app.control.inspect()`; +write operations use `app.control` and require live workers. + +## get_worker_status + +Derive worker status from the event log: `online` / `offline` / `unknown`, +based on `WORKER_OFFLINE_TIMEOUT_SECONDS`. Includes heartbeat details and the +last event type. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/workers +``` + +## list_workers + +Ping all workers and list who is alive. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/workers/list +``` + +## get_worker_stats + +Detailed statistics for a specific worker (requires the worker to respond). + +```bash +curl -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/workers/celery@worker1/stats +``` + +## get_active_tasks + +Currently executing tasks across all workers, or a specific worker. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/workers/active-tasks +curl -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/workers/active-tasks?worker_name=celery@worker1" +``` + +## get_scheduled_tasks + +Tasks with an ETA/countdown waiting in workers' queues. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/workers/scheduled +``` + +## get_reserved_tasks + +Tasks prefetched by a worker but not yet started. + +```bash +curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/workers/reserved +``` + +## shutdown_worker + +Gracefully shut down a worker (waits for current tasks). + +```bash +curl -X POST -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/workers/celery@worker1/shutdown +``` + +## scale_worker_pool + +Grow or shrink a worker's pool size. + +```bash +# Add 2 processes +curl -X POST -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/workers/celery@worker1/scale?delta=2" +# Remove 1 process +curl -X POST -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/workers/celery@worker1/scale?delta=-1" +``` + +## restart_worker_pool + +Restart a worker's execution pool, optionally reloading modules. + +```bash +curl -X POST -H "Authorization: Bearer $API_KEY" \ + http://localhost:8000/api/workers/celery@worker1/restart +# With module reload +curl -X POST -H "Authorization: Bearer $API_KEY" \ + "http://localhost:8000/api/workers/celery@worker1/restart?reload=true" +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..3dfb2a0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,64 @@ +site_name: taskowl +site_description: Modern Celery task monitoring with MCP integration +site_url: https://kalvadtech.github.io/taskowl/ +repo_url: https://github.com/KalvadTech/taskowl +edit_uri: edit/main/docs/ +theme: + name: material + logo: logo.png + favicon: logo.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.tabs.link +nav: + - Overview: index.md + - Setup: + - setup/index.md + - Installation: setup/installation.md + - Configuration: setup/configuration.md + - Usage Guide: + - usage/index.md + - Configuring Celery: usage/celery-app.md + - MCP Clients: usage/mcp-clients.md + - Tasks: usage/tasks.md + - Task Actions: usage/task-actions.md + - Workers: usage/workers.md + - Queues: usage/queues.md + - Automations: usage/automations.md + - Metrics: usage/metrics.md + - Troubleshooting: troubleshooting.md + - Contributing: contributing.md +markdown_extensions: + - admonition + - tables + - toc: + permalink: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/KalvadTech/taskowl +copyright: Copyright © 2026 Kalvad \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cd7b379..423acca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,10 @@ dev = [ "ty==0.0.80", "aiosqlite==0.22.1", ] +docs = [ + "mkdocs>=1.6.1", + "mkdocs-material>=9.7.7", +] [project.scripts] taskowl = "taskowl.main:main" diff --git a/uv.lock b/uv.lock index b7b21ac..1376021 100644 --- a/uv.lock +++ b/uv.lock @@ -104,6 +104,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + [[package]] name = "billiard" version = "4.2.4" @@ -201,6 +223,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] +[[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.4.2" @@ -325,6 +441,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "greenlet" version = "3.5.4" @@ -457,6 +585,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -511,6 +651,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, ] +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -579,6 +728,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.44.0" @@ -600,6 +827,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -732,6 +986,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/20/bb1b39c1f487e6745bfdca09625db8e8265cdce70d2bac679d930a6cbf72/pymdown_extensions-12.0.tar.gz", hash = "sha256:d00b32be02715676bfec9183371928d1db44e1cd58dbeb76c7f27d9f72682039", size = 867035, upload-time = "2026-09-15T03:32:18.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/8c/18eb38daca41f784dd06edc2ef95a75d7c7572e4105f486a5b2cd613081b/pymdown_extensions-12.0-py3-none-any.whl", hash = "sha256:ae9ee9bfd1c1e3d291d6f6ff1883a7ef388594905e80c0c714b9c596fd8a5cb7", size = 277140, upload-time = "2026-09-15T03:32:17.487Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -803,6 +1070,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "redis" version = "8.1.0" @@ -825,6 +1130,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[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 = "rpds-py" version = "2026.6.3" @@ -998,6 +1318,10 @@ dev = [ { name = "ruff" }, { name = "ty" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, +] [package.metadata] requires-dist = [ @@ -1023,6 +1347,10 @@ dev = [ { name = "ruff", specifier = "==0.16.7" }, { name = "ty", specifier = "==0.0.80" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.7.7" }, +] [[package]] name = "truststore" @@ -1100,6 +1428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "uvicorn" version = "0.52.4" @@ -1122,6 +1459,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "wcwidth" version = "0.8.2"