diff --git a/.dockerignore b/.dockerignore
index ae7e651..12bc668 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -37,6 +37,8 @@ conftest.py
**/terraform.tfvars*
**/*.tfvars
**/.env
+**/.env.dev
+dev/.env.dev
deploy.ps1
Makefile
@@ -49,4 +51,4 @@ tfplan
kube_prom_matches.txt
kube_prom_values.txt
loki_stack_matches.txt
-loki_stack_values.txt
\ No newline at end of file
+loki_stack_values.txt
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cc05506..985ddc9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -132,9 +132,9 @@ jobs:
- name: Build Docker images
run: |
- docker build -f docker/dockerfile.celery -t local/celery:test .
- docker build -f docker/dockerfile.fastapi -t local/fastapi:test .
- docker build -f docker/dockerfile.react -t local/react:test .
+ docker build -f docker/Dockerfile.celery -t local/celery:test .
+ docker build -f docker/Dockerfile.fastapi -t local/fastapi:test .
+ docker build -f docker/Dockerfile.react -t local/react:test .
- name: Scan FastAPI image with Trivy
uses: aquasecurity/trivy-action@master
@@ -144,6 +144,7 @@ jobs:
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
+ skip-files: '**/pip/_vendor/bom.cdx.json'
- name: Scan Celery image with Trivy
uses: aquasecurity/trivy-action@master
@@ -153,6 +154,7 @@ jobs:
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
+ skip-files: '**/pip/_vendor/bom.cdx.json'
- name: Scan React image with Trivy
uses: aquasecurity/trivy-action@master
@@ -161,4 +163,4 @@ jobs:
format: 'table'
exit-code: '1'
ignore-unfixed: true
- severity: 'CRITICAL,HIGH'
\ No newline at end of file
+ severity: 'CRITICAL,HIGH'
diff --git a/.gitignore b/.gitignore
index cb48103..56a28bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,8 @@ __pycache__/
.env
venv/
.venv/
+**/.env.dev
+dev/.env.dev
# OS
.DS_Store
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..b3746b8
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,70 @@
+default_install_hook_types: [pre-commit, pre-push]
+
+repos:
+ # =========================================================================
+ # 1. git commit
+ # =========================================================================
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.16.0
+ hooks:
+ - id: ruff
+ args: [--fix]
+ files: ^mona_core/
+ stages: [pre-commit]
+ - id: ruff-format
+ files: ^mona_core/
+ stages: [pre-commit]
+
+ - repo: https://github.com/adrienverge/yamllint
+ rev: v1.38.0
+ hooks:
+ - id: yamllint
+ args: [-c, .yamllint.yaml]
+ stages: [pre-commit]
+
+ - repo: https://github.com/gitleaks/gitleaks
+ rev: v8.30.0
+ hooks:
+ - id: gitleaks
+ stages: [pre-commit]
+
+ - repo: https://github.com/antonbabenko/pre-commit-terraform
+ rev: v1.108.1
+ hooks:
+ - id: terraform_fmt
+ stages: [pre-commit]
+
+ # =========================================================================
+ # 2. git push
+ # =========================================================================
+
+ - repo: local
+ hooks:
+ - id: mypy
+ name: Mypy type check (uv)
+ entry: uv run mypy mona_core/
+ language: system
+ pass_filenames: false
+ stages: [pre-push]
+
+ - id: pytest
+ name: Pytest (uv)
+ entry: uv run pytest tests/
+ language: system
+ pass_filenames: false
+ stages: [pre-push]
+
+ - id: terraform-validate
+ name: Terraform validate
+ entry: bash -c "cd terraform && terraform init -backend=false && terraform validate"
+ language: system
+ pass_filenames: false
+ stages: [pre-push]
+
+ - id: helm-lint
+ name: Helm lint
+ entry: helm lint ./mona-chart
+ language: system
+ pass_filenames: false
+ stages: [pre-push]
\ No newline at end of file
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
index 0000000..c4eef45
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,17 @@
+extends: default
+
+ignore: |
+ mona-chart/templates/
+ .github/workflows/
+
+rules:
+ line-length:
+ max: 200
+ braces:
+ min-spaces-inside: 0
+ max-spaces-inside: 1
+ document-start: disable
+ new-line-at-end-of-file: disable
+ new-lines: disable
+ trailing-spaces: disable
+ comments-indentation: disable
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fb7dbd..b76edd5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,38 @@
# CHANGELOG
All notable changes to this project will be documented in this file.
+## [v0.7.0] - 2026-09-14 - Major update
+### Added
+- backend: Added API versioning except probes and prometheus endpoints.
+- backend: Added return type annotations to API endpoints.
+- backend: Added Pydantic schemes in response model for each enpoints except probes and prometheus endpoints.
+- backend: Added formatter for HTTPExceptions like [MessageResponse]()
+- backend: Added manual CRUD user for admins.
+- backend: Added pagination for devices[GET] endpoint.
+- backend: Added redis to health probe.
+- backend: Added CRUD for users.
+- backend: Added `docker-compose.dev.yml` for development.
+- backend: Added `docker-compose.prod.yml` as second way to setup MONA.
+- bachend: Added logger.
+- bachend: Added formatter for logger.
+- frontend: Added page `userboard`.
+- frontend: Added config with `API_URL` and `API_VERSION`.
+- frontend: Added formatter for `status codes` and `error` into `apiFetch`.
+
+### Changed
+- backend: Moved to `uv sync` & `uv lock` from `pip install` in docker images.
+- backend: Moved to `httpx2` from `requests`.
+- frontend: Split Dashboard.tsx and DeviceAdmin.tsx to components.
+- frontend: Split endpoints to folder routes on backend.
+- frontend: Moved to axios from fetch.
+- frontend: Moved API versioning to custom `apiFetch` on frontend.
+
## [v0.6.0] - 2026-07-28 - Security update
### Added
-- Added limiter for login
-- Added config for .env
-- Added security checks into CI pipeline
-- Added tests for limiter
+- Added limiter for login.
+- Added config via pydantic-settings.
+- Added security checks into CI pipeline.
+- Added tests for limiter.
### Changed
- Changed docker images from docker hub to local
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e32058d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,76 @@
+# Contributing to MONA
+
+Thank you for considering contributing to MONA! This document provides guidelines and instructions for contributing code, reporting issues, and setting up your local development environment.
+
+---
+
+## Local Development Setup
+Requirements:
+* **Python 3.13+**
+* [**uv**](https://github.com/astral-sh/uv) for packages
+* **Docker Desktop** / **kind** / **helm** / **Terraform** for IaC
+
+
+### 1. Clone the repository
+```bash
+git clone [https://github.com/gwill1337/MONA.git](https://github.com/gwill1337/MONA.git)
+cd MONA
+```
+
+### 2. Install dependencies
+```bash
+uv sync --frozen --group dev
+```
+
+### 3. Set up pre-commit & pre-push hooks
+```
+uv run pre-commit install --hook-type pre-commit --hook-type pre-push
+```
+
+
+## Running Checks Manually
+```bash
+# Run pre-commit checks on all files
+uv run pre-commit run --hook-stage pre-commit --all-files
+
+# Run pre-push checks (Mypy & Pytest)
+uv run pre-commit run --hook-stage pre-push --all-files
+
+# Run tests directly with pytest
+uv run pytest tests/
+```
+
+## MONA's dev build
+For fust testing and preview use `docker-compose.dev.yml` with `Dockerfile.dev` and `.env.dev`
+```bash
+docker compose -p mona-dev --env-file dev/.env.dev -f dev/docker-compose.dev.yml up -d --build
+```
+
+## Workflow & Branching
+### 1. Fork or Create a Branch
+Always create a descriptive feature branch off the dev branch:
+```bash
+git checkout dev
+git pull origin dev
+git checkout -b feature/short-description
+# or
+git checkout -b fix/short-description
+```
+### 2. Commit Guidelines
+* Keep commits atomic and focused.
+* Write clear and concise commit messages.
+* Do not use `--no-verify` to bypass Git hooks unless absolutely necessary.
+
+### 3. Submitting a Pull Request (PR)
+* Push your branch to GitHub and open a PR targeting the `dev` branch.
+* Provide a concise description of your changes and reference any related issues.
+* Ensure all automated GitHub Actions CI checks pass (Gitleaks, Checkov, Ruff, Mypy, Pytest, Docker Trivy scan).
+
+## Coding Standards & Linting
+* **Python:** Core code (`mona_core/`) must strictly pass `Ruff` (linting & formatting) and `Mypy` type checks. Test files are exempt from strict Ruff rules (e.g., `assert` statements).
+* **Infrastructure:** Ensure Terraform files are formatted using `terraform fmt` and Helm charts pass `helm lint`.
+* **Security:** Never commit real API keys, passwords, or secrets. `Gitleaks` will reject commits with hardcoded credentials.
+
+## Reporting Bugs & Requesting Features
+* Use **GitHub Issues** to report bugs or request new features.
+* Include reproduction steps, environment details (OS, Docker version, etc.), and relevant logs when reporting bugs.
\ No newline at end of file
diff --git a/Makefile b/Makefile
index b5cab36..6c57241 100644
--- a/Makefile
+++ b/Makefile
@@ -8,12 +8,13 @@ help:
build:
@echo "=== Building Docker images ==="
- docker build -t mona-fastapi:local -f ./docker/dockerfile.fastapi .
- docker build -t mona-react:local -f ./docker/dockerfile.react .
- docker build -t mona-celery:local -f ./docker/dockerfile.celery .
+ docker build -t mona-fastapi:local -f ./docker/Dockerfile.fastapi .
+ docker build -t mona-react:local -f ./docker/Dockerfile.react .
+ docker build -t mona-celery:local -f ./docker/Dockerfile.celery .
deploy:
@echo "=== Deploying infrastructure (Terraform) ==="
+ helm repo update
terraform -chdir=terraform init
terraform -chdir=terraform apply -auto-approve
diff --git a/README.md b/README.md
index c50a6ad..104b8f2 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,61 @@
# MONA - Monitor & Analytics Tool
+
+
+
+
+
+
+
+
+
+
+
+
## About
MONA is a K8s-based monitoring and analytics tool managed with Terraform and Helm. It collects and analyzes system metrics, built on Python with Celery and Redis as a task broker for metrics collection and ML workloads. FastAPI serves as the REST API/backend, React + Tailwind v4 + TypeScript handles web UI frontend, and PostgreSQL stores all metrics data.
## Quick Start:
+### Terraform:
1. Ensure [**Docker Desktop**](https://www.docker.com/products/docker-desktop/), [**kind**](https://github.com/kubernetes-sigs/kind), [**helm**](https://helm.sh/) and [**Terraform**](https://developer.hashicorp.com/terraform/install) are installed.
2. Set up node-exporter on the device you want to monitor with port 9100.
3. Clone repo `git clone https://github.com/gwill1337/MONA.git`
-4. Configure the necessary values in "mona-chart/[values](https://github.com/gwill1337/MONA/blob/main/mona-chart/values.yaml)"[ConfigurationGuide](https://github.com/gwill1337/MONA/blob/dev/ConfigurationGuide.md). *such as PC's IP & name or just add them via **Admin panel***
+4. Configure the necessary values in "mona-chart/[values](https://github.com/gwill1337/MONA/blob/main/mona-chart/values.yaml)" [[ConfigurationGuide]](https://github.com/gwill1337/MONA/blob/dev/ConfigurationGuide.md). *such as PC's IP & name or just add them via **Admin panel***
5. Configure `config.py` for ML and FastAPI limiter settings.
6. Create `terraform.tfvars` in terraform folder from `terraform.tfvars.example`.
-7. Deploy: Run the automated script:
+7. Deploy: Run the automated script:
+
+**Windows:**
```Powershell
.\deploy.ps1 -all # or .\deploy.ps1 -deploy
```
-Or via makefile:
+**Linux/MacOS:**
```makefile
make all
```
*For manual deployment, use terraform init && terraform apply inside the /terraform folder.*
+### Docker-compose (without grafana and alerting):
+1. Ensure [**Docker Desktop**](https://www.docker.com/products/docker-desktop/) are installed.
+2. Set up node-exporter on the device you want to monitor with port 9100.
+3. Clone repo `git clone https://github.com/gwill1337/MONA.git`
+4. Copy `.env.example` to `.env`
+5. Run the `docker-compose.prod.yml`:
+```bash
+docker compose -p mona-prod --env-file .env -f docker-compose.prod.yml up -d --build
+```
+*The Docker Compose version comes without Grafana, Prometheus (for the cluster), and Alertmanager.*
+
## Usage
**Links below are available after setup.**
### Main links:
-* **Admin panel:** `localhost:30081/admin` *Default username: admin, password: admin123*
-* **Dashboards:** `localhost:30081/admin/dashboard`
+* **Admin panel:** `localhost:30081/` *Default username: admin, password: admin123*
+* **Dashboards:** `localhost:30081/dashboard`
+* **Userboard:** `localhost:30081/userboard`
* **Grafana:** `localhost:30300/` *Default username: admin, password: admin123*
-* **Prometheus(Cluster):** `localhost:30091/`
* **Prometheus(Monitoring devices):** `localhost:30391/`
+* **Prometheus(Cluster):** `localhost:30091/`
Click to view detailed API endpoints
@@ -36,24 +63,42 @@ make all
### API:
*P.S. All endpoint require authentification except prometheus and probes.*
* Swagger: `localhost:30080/docs`
-* Main-metrics[Get]: `localhost:30080/db-metrics`
-* Prometheus targets[Get]: `localhost:30080/api/prometheus/targets`
-* Anomalies[Get]: `localhost:30080/anomalies`
-* Model-info[Get]: `localhost:30080/model-info`
-* Devices[Get]: `localhost:30080/devices`
-* Devices[Post]: `localhost:30080/devices` *available via curl*
-* Devices[Delete]: `localhost:30080/devices/{device_id}` *available via curl*
-* Train[Post]: `localhost:30080/train` *available via curl*
-* Model[Delete]: `localhost:30080/model` *available via curl*
-* Dashboard[Get]: `localhost:30080/api/dashboard`
+
+#### Users:
+* Users[Get]: `localhost:30080/api/v1/users`
+* User[Post]: `localhost:30080/api/v1/user`
+* User[Delete]: `localhost:30080/api/v1/user/{user_id}`
+* UserChangePassword[Patch]: `localhost:30080/api/v1/user/change-password`
+* UserChangeRole[Patch]: `localhost:30080/api/v1/user/{user_id}/role`
+
+#### Devices:
+* Devices[Get]: `localhost:30080/api/v1/devices`
+* Devices[Post]: `localhost:30080/api/v1/devices`
+* Devices[Delete]: `localhost:30080/api/v1/devices/{device_id}`
+
+#### Model:
+* Model-info[Get]: `localhost:30080/api/v1/model-info`
+* Train[Post]: `localhost:30080/api/v1/train`
+* Model[Delete]: `localhost:30080/api/v1/model`
+
+#### Monitoring:
+* Dashboard[Get]: `localhost:30080/api/v1/dashboard`
+* TaskStatus[Get]: `localhost:30080/api/v1/task-status/{task_id}`
+* Anomalies[Get]: `localhost:30080/api/v1/anomalies`
+* Metrics[Get]: `localhost:30080/api/v1/db-metrics`
+
+#### Authentication:
+* Login[Post]: `localhost:30080/api/v1/auth/login`
+* Logout[Post]: `localhost:30080/api/v1/auth/logout`
+* Check auth[Get]: `localhost:30080/api/v1/auth/me`
+
#### Probes:
* Liveness[Get]: `localhost:30080/health/live`
* Readiness[Get]: `localhost:30080/health/ready`
-#### Authentication:
-* Login[Post]: `localhost:30080/api/auth/login`
-* Logout[Post]: `localhost:30080/api/auth/logout`
-* Check auth[Get]: `localhost:30080/api/auth/me`
+#### Prometheus Endpoints:
+* Metrics[Get]: `localhost:30080/metrics`
+* Targets[Get]: `localhost:30080/api/prometheus/targets`
@@ -66,7 +111,7 @@ Helm templates for flexible and fast setup can be configured in [values](https:/
* **API Engine:** FastAPI endpoints for handling client requests.
* **Task Queue:** Celery workers with a Redis broker for background metrics collection and ML tasks.
* **PostgreSQL:** For storing metrics and analytics data.
-* **Redis:** As broker for celery and as storage for sessions and limiter.
+* **Redis:** As a broker for celery and as a storage for sessions and limiter.
* **Monitoring Stack:** **Prometheus** for data scraping, **Grafana** for visualization (Recharts is also used in the web UI), Alertmanager for pod alert notifications, and **Loki** with **Promtail** for pod logs.

@@ -95,7 +140,7 @@ and can be opened via psql:
kubectl exec -it statefulset/postgres-statefulset -n mona -- psql -U myuser -d mydb
# ⬆ pod name ⬆ namespace ⬆ Username ⬆ DB name
```
-
+
## Authentication & Authorization
MONA uses a stateful, cookie-based authentication system backed by Redis to secure the admin panel and core API endpoints.
@@ -119,7 +164,7 @@ Automated checks and docker build & push run on every push and pull request:
* **Helm** — lint for helm charts
* **YAML** — lint for values and chart files
* **Python** — ruff (lint and format checks), MyPy (type checks), Pytest (Api tests)
-* **Docker** - scan images via Trivy
+* **Docker** — scan images via Trivy
## Architecture
Here more about architecture and how mona works.
diff --git a/deploy.ps1 b/deploy.ps1
index 9f95e9e..9b585cc 100644
--- a/deploy.ps1
+++ b/deploy.ps1
@@ -6,13 +6,14 @@ param (
if ($All -or $Build) {
Write-Host "=== Building Docker images ===" -ForegroundColor Cyan
- docker build -t mona-fastapi:local -f ./docker/dockerfile.fastapi .
- docker build -t mona-react:local -f ./docker/dockerfile.react .
- docker build -t mona-celery:local -f ./docker/dockerfile.celery .
+ docker build -t mona-fastapi:local -f ./docker/Dockerfile.fastapi .
+ docker build -t mona-react:local -f ./docker/Dockerfile.react .
+ docker build -t mona-celery:local -f ./docker/Dockerfile.celery .
}
if ($All -or $Deploy) {
Write-Host "=== Deploying infrastructure (Terraform) ===" -ForegroundColor Cyan
+ helm repo update
terraform -chdir=terraform init
terraform -chdir=terraform apply -auto-approve
}
diff --git a/dev/Dockerfile.dev.fastapi b/dev/Dockerfile.dev.fastapi
new file mode 100644
index 0000000..814e928
--- /dev/null
+++ b/dev/Dockerfile.dev.fastapi
@@ -0,0 +1,23 @@
+FROM python:3.13-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential curl \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN pip install --no-cache-dir uv
+
+WORKDIR /app
+
+# кэшируем зависимости отдельным слоем
+COPY pyproject.toml uv.lock* ./
+RUN uv sync --frozen --no-install-project || uv sync --no-install-project
+
+COPY . .
+
+RUN uv sync --frozen || uv sync
+
+ENV PATH="/app/.venv/bin:$PATH"
+
+EXPOSE 8000
+
+CMD ["uv", "run", "uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
\ No newline at end of file
diff --git a/dev/Dockerfile.dev.react b/dev/Dockerfile.dev.react
new file mode 100644
index 0000000..2dd2ff2
--- /dev/null
+++ b/dev/Dockerfile.dev.react
@@ -0,0 +1,10 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY frontend/package*.json ./
+RUN npm ci
+
+EXPOSE 5173
+
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
\ No newline at end of file
diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml
new file mode 100644
index 0000000..4dbe5b4
--- /dev/null
+++ b/dev/docker-compose.dev.yml
@@ -0,0 +1,177 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: mona-postgres-dev
+ restart: unless-stopped
+ env_file:
+ - .env.dev
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env.dev}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env.dev}
+ POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env.dev}
+ ports:
+ - "5432:5432"
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:?set POSTGRES_USER in .env.dev} -d ${POSTGRES_DB:?set POSTGRES_DB in .env.dev}"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
+ redis:
+ image: redis:7-alpine
+ container_name: mona-redis-dev
+ restart: unless-stopped
+ command: ["redis-server", "--appendonly", "yes"]
+ ports:
+ - "6379:6379"
+ volumes:
+ - redisdata:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
+ api:
+ build:
+ context: ..
+ dockerfile: dev/Dockerfile.dev.fastapi
+ image: mona-api-dev:local
+ container_name: mona-fastapi-dev
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ env_file:
+ - .env.dev
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-DEBUG}
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]}
+ ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin}
+ ADMIN_PASSWORDS: ${ADMIN_PASSWORDS:-admin123}
+ USER_USERNAMES: ${USER_USERNAMES:-}
+ USER_PASSWORDS: ${USER_PASSWORDS:-}
+ ports:
+ - "30080:8000"
+ volumes:
+ - ..:/app
+ - api_venv:/app/.venv
+ working_dir: /app
+ command: >
+ sh -c "uv run alembic upgrade head &&
+ uv run uvicorn mona_core.main:app --host 0.0.0.0 --port 8000 --reload"
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/live')"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 15s
+
+ celery-worker:
+ build:
+ context: ..
+ dockerfile: dev/Dockerfile.dev.fastapi
+ image: mona-api-dev:local
+ container_name: mona-celery-worker-dev
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ api:
+ condition: service_healthy
+ env_file:
+ - .env.dev
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-DEBUG}
+ PYTHONPATH: /app/mona_core
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ PROMETHEUS_URL: http://prometheus:9090
+ # Comma-separated jobName:host pairs for physical PCs, e.g. "pc-test:10.0.0.8"
+ # Leave empty and manage devices via the Admin panel instead, if you prefer.
+ EXPORTERS: ${EXPORTERS:-}
+ volumes:
+ - ..:/app
+ - api_venv:/app/.venv
+ working_dir: /app
+ command: uv run celery -A celery_conf worker --loglevel=info
+
+ celery-beat:
+ build:
+ context: ..
+ dockerfile: dev/Dockerfile.dev.fastapi
+ image: mona-api-dev:local
+ container_name: mona-celery-beat-dev
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ env_file:
+ - .env.dev
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-DEBUG}
+ PYTHONPATH: /app/mona_core
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ volumes:
+ - ..:/app
+ - api_venv:/app/.venv
+ working_dir: /app
+ command: uv run celery -A celery_conf beat --loglevel=info -s /tmp/celerybeat-schedule
+
+ react:
+ build:
+ context: ..
+ dockerfile: dev/Dockerfile.dev.react
+ container_name: mona-react-dev
+ restart: unless-stopped
+ depends_on:
+ - api
+ volumes:
+ - ../frontend:/app
+ - react_node_modules:/app/node_modules
+ ports:
+ - "5173:5173"
+ environment:
+ CHOKIDAR_USEPOLLING: "true"
+
+ prometheus:
+ image: prom/prometheus:v2.55.1
+ container_name: mona-prometheus-dev
+ restart: unless-stopped
+ volumes:
+ - ../prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - prometheus-data:/prometheus
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.path=/prometheus
+ - --web.enable-lifecycle
+ ports:
+ - "30391:9090"
+ depends_on:
+ - api
+
+volumes:
+ pgdata:
+ name: mona_pgdata_dev
+ redisdata:
+ name: mona_redisdata_dev
+ api_venv:
+ name: mona_api_venv_dev
+ react_node_modules:
+ name: mona_react_node_modules_dev
+ prometheus-data:
+ name: mona_prometheus_data_dev
\ No newline at end of file
diff --git a/dev/env.dev.example b/dev/env.dev.example
new file mode 100644
index 0000000..778382a
--- /dev/null
+++ b/dev/env.dev.example
@@ -0,0 +1,19 @@
+# Copy to .env.dev and fill in real values before running docker compose up.
+# Log level like ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), "DEBUG" by default
+LOG_LEVEL=DEBUG
+
+POSTGRES_USER=myuser123
+POSTGRES_PASSWORD=changeme
+POSTGRES_DB=mydb
+
+POSTGRES_PORT=5432
+
+ADMIN_USERNAMES=admin
+ADMIN_PASSWORDS=admin123
+USER_USERNAMES=user1
+USER_PASSWORDS=userpass1
+
+CORS_ORIGINS=["http://localhost:30080","http://localhost:5173"]
+
+# Comma-separated jobName:host pairs for physical PCs, e.g. pc-office:10.0.0.8,pc-lab:10.0.0.9
+EXPORTERS=
\ No newline at end of file
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..de3f5a4
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,206 @@
+services:
+ # ─── PostgreSQL ─────────────────────────────────────────────────────────
+ postgres:
+ image: postgres:16-alpine
+ container_name: mona-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
+ POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ # Uncomment only if you need psql access from the host:
+ # ports:
+ # - "5432:5432"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:?set POSTGRES_USER in .env} -d ${POSTGRES_DB:?set POSTGRES_DB in .env}"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ networks:
+ - mona
+
+ # ─── Redis (celery broker + sessions/limiter storage) ──────────────────
+ redis:
+ image: redis:7-alpine
+ container_name: mona-redis
+ restart: unless-stopped
+ command: ["redis-server", "--appendonly", "yes"]
+ volumes:
+ - redisdata:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ networks:
+ - mona
+
+ # ─── FastAPI ─────────────────────────────────────────────────────────
+ api:
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile.fastapi
+ image: mona-fastapi:local
+ container_name: mona-fastapi
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-INFO}
+ UV_NO_CACHE: "1"
+ POSTGRES_USER: ${POSTGRES_USER:-myuser}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
+ POSTGRES_DB: ${POSTGRES_DB:-mydb}
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]}
+ ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin}
+ ADMIN_PASSWORDS: ${ADMIN_PASSWORDS:?set ADMIN_PASSWORDS in .env}
+ USER_USERNAMES: ${USER_USERNAMES:-}
+ USER_PASSWORDS: ${USER_PASSWORDS:-}
+ command: >
+ sh -c "alembic upgrade head &&
+ uvicorn mona_core.main:app --host 0.0.0.0 --port 8000"
+ ports:
+ - "30080:8000"
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/live')"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 15s
+ networks:
+ - mona
+
+ # ─── Celery worker (metrics collection + ML training/inference) ───────
+ celery-worker:
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile.celery
+ image: mona-celery:local
+ container_name: mona-celery-worker
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ api:
+ condition: service_healthy
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-INFO}
+ POSTGRES_USER: ${POSTGRES_USER:-myuser}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
+ POSTGRES_DB: ${POSTGRES_DB:-mydb}
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ PROMETHEUS_URL: http://prometheus:9090
+ # Comma-separated jobName:host pairs for physical PCs, e.g. "pc-test:10.0.0.8"
+ # Leave empty and manage devices via the Admin panel instead, if you prefer.
+ EXPORTERS: ${EXPORTERS:-}
+ command: celery -A mona_core.celery_conf worker --loglevel=info
+ healthcheck:
+ test: ["CMD-SHELL", "celery -A mona_core.celery_conf inspect ping -d celery@$$HOSTNAME || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 20s
+ networks:
+ - mona
+
+ # ─── Celery beat (schedules collect_and_save + detect_anomalies) ──────
+ celery-beat:
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile.celery
+ image: mona-celery:local
+ container_name: mona-celery-beat
+ restart: unless-stopped
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ environment:
+ LOG_LEVEL: ${LOG_LEVEL:-INFO}
+ POSTGRES_USER: ${POSTGRES_USER:-myuser}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
+ POSTGRES_DB: ${POSTGRES_DB:-mydb}
+ POSTGRES_HOST: postgres
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ REDIS_URL: redis://redis:6379
+ command: celery -A mona_core.celery_conf beat --loglevel=info -s /tmp/celerybeat-schedule
+ volumes:
+ - celerybeat-tmp:/tmp
+ healthcheck:
+ test: ["CMD-SHELL", "test -f /tmp/celerybeat-schedule"]
+ interval: 30s
+ timeout: 5s
+ retries: 5
+ start_period: 20s
+ networks:
+ - mona
+
+ # ─── React frontend ─────────────────────────────────────────────────
+ react:
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile.react
+ image: mona-react:local
+ container_name: mona-react
+ restart: unless-stopped
+ depends_on:
+ - api
+ ports:
+ - "30081:8080"
+ read_only: true
+ tmpfs:
+ - /tmp
+ - /var/cache/nginx
+ - /var/run/nginx
+ networks:
+ - mona
+
+ # ─── Prometheus — MONA's OWN product monitoring (physical PCs) ────────
+ # This is NOT cluster/infra monitoring — there is no Grafana, Loki,
+ # Alertmanager or kube-prometheus-stack equivalent here. Those exist
+ # only in the Helm/Terraform deployment for observing the k8s cluster
+ # itself. This Prometheus instance scrapes the monitored devices via
+ # node-exporter, same as the "Prometheus(Monitoring devices)" in the
+ # k8s setup — it's core product functionality, not infra tooling.
+ prometheus:
+ image: prom/prometheus:v2.55.1
+ container_name: mona-prometheus
+ restart: unless-stopped
+ volumes:
+ - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - prometheus-data:/prometheus
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.path=/prometheus
+ - --web.enable-lifecycle
+ ports:
+ - "30391:9090"
+ depends_on:
+ - api
+ networks:
+ - mona
+
+volumes:
+ pgdata:
+ name: mona_pgdata_prod
+ redisdata:
+ name: mona_redisdata_prod
+ prometheus-data:
+ celerybeat-tmp:
+
+networks:
+ mona:
+ driver: bridge
\ No newline at end of file
diff --git a/docker/Caddyfile b/docker/Caddyfile
deleted file mode 100644
index 53f269c..0000000
--- a/docker/Caddyfile
+++ /dev/null
@@ -1,9 +0,0 @@
-:8080
-
-root * /usr/share/caddy
-
-encode gzip zstd
-
-file_server
-
-try_files {path} /index.html
\ No newline at end of file
diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery
new file mode 100644
index 0000000..e44268c
--- /dev/null
+++ b/docker/Dockerfile.celery
@@ -0,0 +1,59 @@
+# FROM python:3.13 AS builder
+
+# RUN apt-get update \
+# && apt-get upgrade -y \
+# && rm -rf /var/lib/apt/lists/*
+
+# COPY docker/requirements_celery.txt .
+# RUN pip install --no-cache-dir -r requirements_celery.txt
+
+# FROM python:3.13-slim
+# RUN apt-get update \
+# && apt-get upgrade -y \
+# && rm -rf /var/lib/apt/lists/*
+# RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
+# COPY --from=builder /usr/local /usr/local
+# COPY . /app
+# WORKDIR /app
+
+# ENV PYTHONPATH=/app/mona_core
+
+# RUN chown -R app:app /app
+# USER app
+# HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import shutil; import subprocess; import sys; sys.exit(0 if shutil.which('celery') else 1)" || exit 1
+# CMD ["celery", "-A", "celery_conf", "worker", "--loglevel=info"]
+
+FROM python:3.13 AS builder
+
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+ENV UV_COMPILE_BYTECODE=1 \
+ UV_LINK_MODE=copy \
+ UV_PROJECT_ENVIRONMENT=/opt/venv
+
+WORKDIR /app
+COPY pyproject.toml uv.lock ./
+
+RUN uv sync --frozen --no-default-groups --group worker --no-install-project
+
+RUN uv pip install --python /opt/venv/bin/python --no-cache \
+ --upgrade "setuptools>=78.1.1" "msgpack>=1.2.1"
+
+FROM python:3.13-slim
+
+RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
+
+
+RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
+COPY . /app
+WORKDIR /app
+
+COPY --from=builder /opt/venv /opt/venv
+
+ENV PATH="/opt/venv/bin:$PATH"
+ENV PYTHONPATH=/app/mona_core
+
+RUN chown -R app:app /app
+USER app
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import shutil; import subprocess; import sys; sys.exit(0 if shutil.which('celery') else 1)" || exit 1
+CMD ["celery", "-A", "celery_conf", "worker", "--loglevel=info"]
diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi
new file mode 100644
index 0000000..fff585b
--- /dev/null
+++ b/docker/Dockerfile.fastapi
@@ -0,0 +1,63 @@
+# FROM python:3.13 AS builder
+
+# WORKDIR /app
+# COPY docker/requirements_fastapi.txt .
+# RUN pip install --no-cache-dir -r requirements_fastapi.txt
+
+# FROM python:3.13-slim
+
+# RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
+
+# COPY --from=builder /usr/local /usr/local
+
+# RUN pip install --no-cache-dir --upgrade "setuptools>=78.1.1" "msgpack>=1.2.1"
+
+# RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
+# COPY . /app
+# WORKDIR /app
+# RUN chown -R app:app /app
+# USER app
+
+# HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
+# CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live', timeout=2)" || exit 1
+
+# CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"]
+
+FROM python:3.13-slim AS builder
+
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+ENV UV_COMPILE_BYTECODE=1 \
+ UV_LINK_MODE=copy \
+ UV_PROJECT_ENVIRONMENT=/opt/venv
+
+WORKDIR /app
+COPY pyproject.toml uv.lock ./
+
+# --frozen : не трогать uv.lock, ставить строго как зафиксировано
+# --no-default-groups : не тянуть группу dev (она синкается по умолчанию)
+# --group docker-fastapi : добавить только нужную для этого образа группу
+# --no-install-project : mona_core не оформлен как устанавливаемый пакет,
+# код копируется просто как файлы ниже
+RUN uv sync --frozen --no-default-groups --group docker-fastapi --no-install-project
+
+RUN uv pip install --python /opt/venv/bin/python --no-cache \
+ --upgrade "setuptools>=78.1.1" "msgpack>=1.2.1"
+
+FROM python:3.13-slim
+
+RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder /opt/venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
+COPY . /app
+WORKDIR /app
+RUN chown -R app:app /app
+USER app
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live', timeout=2)" || exit 1
+
+CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/docker/dockerfile.react b/docker/Dockerfile.react
similarity index 82%
rename from docker/dockerfile.react
rename to docker/Dockerfile.react
index 63a273a..324f111 100644
--- a/docker/dockerfile.react
+++ b/docker/Dockerfile.react
@@ -13,6 +13,12 @@ RUN npm run build
# ---------- Runtime ----------
FROM nginxinc/nginx-unprivileged:stable-alpine
+USER root
+
+RUN apk update \
+ && apk upgrade \
+ && rm -rf /var/cache/apk/*
+
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
@@ -31,4 +37,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD nginx -t || exit 1
-CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/docker/dockerfile.celery b/docker/dockerfile.celery
deleted file mode 100644
index 0d7ea95..0000000
--- a/docker/dockerfile.celery
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM python:3.13 AS builder
-COPY docker/requirements_celery.txt .
-RUN pip install --no-cache-dir -r requirements_celery.txt
-
-FROM python:3.13-slim
-# RUN addgroup --system app && adduser --system --ingroup app app
-RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
-COPY --from=builder /usr/local /usr/local
-COPY . /app
-WORKDIR /app
-
-ENV PYTHONPATH=/app/mona_core
-
-RUN chown -R app:app /app
-USER app
-HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import shutil; import subprocess; import sys; sys.exit(0 if shutil.which('celery') else 1)" || exit 1
-CMD ["celery", "-A", "celery_conf", "worker", "--loglevel=info"]
\ No newline at end of file
diff --git a/docker/dockerfile.fastapi b/docker/dockerfile.fastapi
deleted file mode 100644
index 690ae6f..0000000
--- a/docker/dockerfile.fastapi
+++ /dev/null
@@ -1,15 +0,0 @@
-FROM python:3.13 AS builder
-WORKDIR /app
-COPY docker/requirements_fastapi.txt .
-RUN pip install --no-cache-dir -r requirements_fastapi.txt
-
-FROM python:3.13-slim
-# RUN addgroup --system app && adduser --system --ingroup app app
-RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app
-COPY --from=builder /usr/local /usr/local
-COPY . /app
-WORKDIR /app
-RUN chown -R app:app /app
-USER app
-HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/live', timeout=2)" || exit 1
-CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"]
\ No newline at end of file
diff --git a/docker/requirements_celery.txt b/docker/requirements_celery.txt
deleted file mode 100644
index 436a188..0000000
--- a/docker/requirements_celery.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-celery
-requests
-sqlalchemy
-psycopg2-binary
-redis
-scikit-learn
-numpy
-PyYAML>=6.0.1
-bcrypt
-pydantic-settings
-skops
\ No newline at end of file
diff --git a/docker/requirements_fastapi.txt b/docker/requirements_fastapi.txt
deleted file mode 100644
index 6b87cdd..0000000
--- a/docker/requirements_fastapi.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-fastapi
-uvicorn
-sqlalchemy
-psycopg2-binary
-prometheus_fastapi_instrumentator
-requests
-celery
-redis
-bcrypt
-dotenv
-alembic
-pydantic-settings
\ No newline at end of file
diff --git a/env.example b/env.example
new file mode 100644
index 0000000..44728cc
--- /dev/null
+++ b/env.example
@@ -0,0 +1,19 @@
+# Copy to .env and fill in real values before running docker compose up.
+# Log level like ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), "INFO" by default
+LOG_LEVEL=INFO
+
+POSTGRES_USER=myuser
+POSTGRES_PASSWORD=changeme
+POSTGRES_DB=mydb
+
+POSTGRES_PORT=5432
+
+ADMIN_USERNAMES=admin
+ADMIN_PASSWORDS=admin123
+USER_USERNAMES=
+USER_PASSWORDS=
+
+CORS_ORIGINS=["http://localhost:30081"]
+
+# Comma-separated jobName:host pairs for physical PCs, e.g. pc-office:10.0.0.8,pc-lab:10.0.0.9
+EXPORTERS=
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 976c7f3..e05155e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.3.1",
+ "axios": "^1.19.0",
"lucide-react": "^1.21.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
@@ -1194,6 +1195,49 @@
}
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
+ "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.6",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1203,6 +1247,18 @@
"node": ">=6"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
@@ -1344,12 +1400,38 @@
"node": ">=12"
}
},
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1359,6 +1441,20 @@
"node": ">=8"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.21.6",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
@@ -1372,6 +1468,51 @@
"node": ">=10.13.0"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-toolkit": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
@@ -1405,6 +1546,42 @@
}
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1419,12 +1596,122 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
@@ -1732,6 +2019,42 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
@@ -1845,6 +2168,15 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index d051266..db0629d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.3.1",
+ "axios": "^1.19.0",
"lucide-react": "^1.21.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx
deleted file mode 100644
index 9003b59..0000000
--- a/frontend/src/Dashboard.tsx
+++ /dev/null
@@ -1,979 +0,0 @@
-import { useState, useEffect, useCallback, useRef } from "react";
-import { useNavigate } from "react-router-dom";
-import { apiFetch } from "./api";
-import {
- LineChart, Line, XAxis, YAxis, CartesianGrid,
- Tooltip, ResponsiveContainer, ReferenceDot, Legend,
-} from "recharts";
-import {
- Activity, AlertTriangle, ArrowLeft, BrainCircuit, ChevronDown,
- CheckSquare, Clock, Cpu, MemoryStick, RefreshCw,
- Server, Square, Trash2, Wifi, WifiOff, X, Zap,
-} from "lucide-react";
-
-// ─── Types ───────────────────────────────────────────────────────────────────
-interface Device {
- id: number;
- ip: string;
- name: string;
- is_active: boolean;
-}
-interface MetricPoint {
- timestamp: string;
- cpu: number;
- ram: number;
- device: string;
-}
-interface Anomaly {
- id: number;
- timestamp: string;
- cpu: number;
- ram: number;
- reason: string;
- score: number;
- device: string;
-}
-interface ModelInfo {
- status: "ok" | "no_model";
- message?: string;
- model?: {
- trained_at: string;
- trained_by: string;
- points_count: number;
- period_from: string;
- period_to: string;
- note: string | null;
- };
-}
-interface DashboardData {
- devices: string[];
- metrics: MetricPoint[];
- anomalies: Anomaly[];
-}
-
-const DEVICE_COLORS = [
- "#22d3ee", "#a78bfa", "#fb923c", "#34d399",
- "#f472b6", "#facc15", "#60a5fa", "#f87171",
-];
-function getDeviceColor(i: number) { return DEVICE_COLORS[i % DEVICE_COLORS.length]; }
-
-// ─── URL helpers ──────────────────────────────────────────────────────────────
-function getUrlDevices(): string[] {
- const params = new URLSearchParams(window.location.search);
- const raw = params.get("device");
- if (!raw) return [];
- return raw.split(",").map((s) => s.trim()).filter(Boolean);
-}
-
-function setUrlDevices(devices: string[]) {
- const params = new URLSearchParams(window.location.search);
- if (devices.length === 0) {
- params.delete("device");
- } else {
- params.set("device", devices.join(","));
- }
- const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
- window.history.replaceState(null, "", newUrl);
-}
-
-// ─── Helpers ──────────────────────────────────────────────────────────────────
-function formatTime(iso: string) {
- // Append Z if no timezone info to treat as UTC and avoid local-shift
- const s = /[Zz]|[+-]\d{2}:\d{2}$/.test(iso) ? iso : iso + "Z";
- return new Date(s).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
-}
-function formatDateTime(iso: string) {
- const s = /[Zz]|[+-]\d{2}:\d{2}$/.test(iso) ? iso : iso + "Z";
- return new Date(s).toLocaleString("ru-RU", {
- day: "2-digit", month: "2-digit",
- hour: "2-digit", minute: "2-digit", second: "2-digit",
- });
-}
-
-// ─── Custom Tooltip ───────────────────────────────────────────────────────────
-function ChartTooltip({ active, payload, label, anomalies }: {
- active?: boolean; payload?: any[]; label?: string; anomalies: Anomaly[];
-}) {
- if (!active || !payload?.length) return null;
- const matching = anomalies.filter((a) => formatTime(a.timestamp) === label);
- return (
-
-
{label}
- {payload.map((e: any) => (
-
-
-
- {String(e.name).replace(/__cpu|__ram/, "")}
-
-
- {typeof e.value === "number" ? e.value.toFixed(1) : e.value}%
-
-
- ))}
- {matching.length > 0 && (
-
- {matching.map((a) => (
-
-
-
- {a.device}:
- {a.reason}
-
-
- ))}
-
- )}
-
- );
-}
-
-// ─── Device Selector ──────────────────────────────────────────────────────────
-function DeviceSelector({ devices, selected, onChange, deviceColorMap }: {
- devices: Device[];
- selected: string[];
- onChange: (s: string[]) => void;
- deviceColorMap: Record;
-}) {
- const [open, setOpen] = useState(false);
- const ref = useRef(null);
-
- useEffect(() => {
- const h = (e: MouseEvent) => {
- if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
- };
- document.addEventListener("mousedown", h);
- return () => document.removeEventListener("mousedown", h);
- }, []);
-
- const toggle = (name: string) => {
- if (selected.includes(name)) {
- if (selected.length === 1) return;
- onChange(selected.filter((d) => d !== name));
- } else {
- onChange([...selected, name]);
- }
- };
-
- const allSelected = devices.length > 0 && devices.every((d) => selected.includes(d.name));
-
- const label = allSelected
- ? "All devices"
- : selected.length === 1
- ? selected[0]
- : `${selected.length} devices`;
-
- return (
-
-
setOpen((o) => !o)}
- className="flex items-center gap-2 px-3 py-2 rounded-xl border border-slate-700/60 bg-slate-900/80 text-sm text-slate-200 hover:border-slate-600 transition-all"
- >
-
- {label}
-
-
-
- {open && (
-
-
-
- {/* All toggle */}
-
- allSelected
- ? onChange(devices.length > 0 ? [devices[0].name] : [])
- : onChange(devices.map((d) => d.name))
- }
- className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-xs text-slate-400 hover:text-white hover:bg-slate-800 transition-all mb-1"
- >
- {allSelected
- ?
- : }
- All devices
-
-
- {devices.map((device) => {
- const checked = selected.includes(device.name);
- const color = deviceColorMap[device.name];
- return (
-
toggle(device.name)}
- className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-sm hover:bg-slate-800 transition-all"
- >
- {checked
- ?
- : }
-
-
- {device.name}
-
- {device.is_active
- ?
- : }
-
- );
- })}
-
-
- )}
-
- );
-}
-
-// ─── Period Selector ──────────────────────────────────────────────────────────
-const PERIODS = [
- { label: "1h", value: 1 },
- { label: "6h", value: 6 },
- { label: "24h", value: 24 },
- { label: "7d", value: 168 },
- { label: "All", value: 0 },
-];
-function PeriodSelector({ value, onChange }: { value: number; onChange: (v: number) => void }) {
- return (
-
- {PERIODS.map((p) => (
- onChange(p.value)}
- className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
- value === p.value ? "bg-slate-700 text-white" : "text-slate-500 hover:text-slate-300"
- }`}
- >
- {p.label}
-
- ))}
-
- );
-}
-
-// ─── Stat Card ────────────────────────────────────────────────────────────────
-function StatCard({ label, value, sub, icon, accent = "text-white" }: {
- label: string; value: string | number; sub?: string; icon: React.ReactNode; accent?: string;
-}) {
- return (
-
-
- {label}
- {icon}
-
-
{value}
- {sub &&
{sub}
}
-
- );
-}
-
-// ─── Anomaly Feed ─────────────────────────────────────────────────────────────
-function AnomalyFeed({ anomalies, deviceColorMap }: {
- anomalies: Anomaly[];
- deviceColorMap: Record;
-}) {
- const scrollRef = useRef(null);
-
- // Auto-scroll to top whenever anomalies change (newest are prepended)
- useEffect(() => {
- if (scrollRef.current) {
- scrollRef.current.scrollTop = 0;
- }
- }, [anomalies]);
-
- if (anomalies.length === 0) {
- return (
-
-
-
No anomalies detected
-
- );
- }
-
- // Already sorted newest-first from API (order_by desc), but reverse just in case
- const sorted = [...anomalies].sort(
- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- );
-
- return (
-
- {sorted.map((a) => {
- const color = deviceColorMap[a.device] ?? "#94a3b8";
- const severity = a.score < -0.15 ? "high" : a.score < -0.05 ? "mid" : "low";
- const sevColor = severity === "high" ? "#f87171" : severity === "mid" ? "#fb923c" : "#facc15";
- return (
-
-
-
-
-
- {a.device}
- {formatDateTime(a.timestamp)}
-
-
{a.reason}
-
- CPU {a.cpu.toFixed(1)}%
- RAM {a.ram.toFixed(1)}%
-
- score {a.score.toFixed(4)}
-
-
-
-
-
- );
- })}
-
- );
-}
-
-// ─── Train status types ───────────────────────────────────────────────────────
-type TrainPhase =
- | { kind: "idle" }
- | { kind: "submitting" }
- | { kind: "polling"; taskId: string; elapsed: number }
- | { kind: "success"; text: string }
- | { kind: "error"; text: string };
-
-// ─── Model Panel ─────────────────────────────────────────────────────────────
-function ModelPanel({ onTrained }: { onTrained: () => void }) {
- const [modelInfo, setModelInfo] = useState(null);
- const [trainHours, setTrainHours] = useState("1");
- const [trainNote, setTrainNote] = useState("");
- const [phase, setPhase] = useState({ kind: "idle" });
- const [deleting, setDeleting] = useState(false);
- const [expanded, setExpanded] = useState(false);
- const pollRef = useRef | null>(null);
-
- const fetchModelInfo = useCallback(async () => {
- try {
- const res = await apiFetch("/model-info");
- setModelInfo(await res.json());
- } catch {}
- }, []);
-
- useEffect(() => { fetchModelInfo(); }, [fetchModelInfo]);
- useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []);
-
- const startPolling = useCallback((taskId: string, prevPoints: number) => {
- let elapsed = 0;
- const INTERVAL = 2000; // poll every 2s
- const TIMEOUT = 120000;
-
- const stop = () => {
- if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
- };
-
- pollRef.current = setInterval(async () => {
- elapsed += INTERVAL;
- setPhase({ kind: "polling", taskId, elapsed });
-
- // ── 1. Check Celery task result via /task-status ──────────────────────
- try {
- const tr = await apiFetch(`/task-status/${taskId}`);
- if (tr.ok) {
- const ts = await tr.json();
- // state: PENDING | STARTED | SUCCESS | FAILURE | RETRY | REVOKED
- if (ts.state === "SUCCESS") {
- const r = ts.result ?? {};
- if (r.status === "error") {
- // Celery finished but returned a domain error (e.g. not enough data)
- stop();
- setPhase({ kind: "error", text: r.message ?? "Training failed" });
- return;
- }
- // Real success — refresh model-info
- stop();
- await fetchModelInfo();
- const infoRes = await apiFetch("/model-info");
- const info: ModelInfo = infoRes.ok ? await infoRes.json() : { status: "no_model" };
- setModelInfo(info);
- const pts = info.status === "ok" ? info.model!.points_count : prevPoints;
- setPhase({ kind: "success", text: `Model trained on ${pts} points` });
- onTrained();
- return;
- }
- if (ts.state === "FAILURE") {
- stop();
- const msg = typeof ts.result === "string" ? ts.result : JSON.stringify(ts.result);
- setPhase({ kind: "error", text: `Worker error: ${msg}` });
- return;
- }
- // PENDING / STARTED / RETRY — keep polling
- }
- } catch {
- // /task-status not available yet — fall through to model-info fallback
- }
-
- // ── 2. Fallback: if task-status endpoint missing, detect via model-info ─
- try {
- const mr = await apiFetch("/model-info");
- if (mr.ok) {
- const info: ModelInfo = await mr.json();
- if (info.status === "ok" && (info.model?.points_count ?? 0) !== prevPoints) {
- stop();
- setModelInfo(info);
- setPhase({ kind: "success", text: `Model trained on ${info.model!.points_count} points` });
- onTrained();
- return;
- }
- }
- } catch {}
-
- if (elapsed >= TIMEOUT) {
- stop();
- setPhase({ kind: "error", text: "Training timed out. Check Celery worker logs." });
- }
- }, INTERVAL);
- }, [fetchModelInfo, onTrained]);
-
- const handleTrain = async () => {
- if (pollRef.current) return;
- const prevPoints = modelInfo?.model?.points_count ?? 0;
- setPhase({ kind: "submitting" });
- try {
- const params = new URLSearchParams({ hours: trainHours, note: trainNote });
- const res = await apiFetch(`/train?${params}`, {
- method: "POST",
- });
- const d = await res.json();
-
- if (d.status === "accepted") {
- setPhase({ kind: "polling", taskId: d.task_id ?? "?", elapsed: 0 });
- startPolling(d.task_id ?? "?", prevPoints);
- } else if (d.status === "ok" || d.status === "success") {
- setPhase({ kind: "success", text: d.message ?? "Model trained successfully" });
- await fetchModelInfo();
- onTrained();
- } else {
- setPhase({ kind: "error", text: d.message ?? "Unknown error from server" });
- }
- } catch (e: any) {
- setPhase({ kind: "error", text: `Network error: ${e.message}` });
- }
- };
-
- const handleDelete = async () => {
- if (!confirm("Reset model and return to auto-mode?")) return;
- setDeleting(true);
- try {
- await apiFetch("/model", {
- method: "DELETE",
- });
- await fetchModelInfo();
- setPhase({ kind: "success", text: "Model deleted. Switched to auto-mode." });
- onTrained();
- } catch (e: any) {
- setPhase({ kind: "error", text: `Failed to delete: ${e.message}` });
- } finally { setDeleting(false); }
- };
-
- const isTraining = phase.kind === "submitting" || phase.kind === "polling";
- const hasModel = modelInfo?.status === "ok";
-
- function StatusBanner() {
- if (phase.kind === "idle") return null;
-
- if (phase.kind === "submitting") return (
-
-
- Submitting task to Celery…
-
- );
-
- if (phase.kind === "polling") {
- const secs = Math.round(phase.elapsed / 1000);
- return (
-
-
-
-
Training in background…
-
{secs}s
-
-
- task_id: {phase.taskId.slice(0, 18)}…
-
-
-
- );
- }
-
- if (phase.kind === "success") return (
-
- ✓
- {phase.text}
- setPhase({ kind: "idle" })} className="ml-auto text-emerald-600 hover:text-emerald-400 transition-colors">
-
-
-
- );
-
- if (phase.kind === "error") return (
-
-
-
-
Training failed
-
setPhase({ kind: "idle" })} className="text-red-700 hover:text-red-400 transition-colors">
-
-
-
-
{phase.text}
-
- );
-
- return null;
- }
-
- return (
-
-
setExpanded((e) => !e)} className="w-full flex items-center gap-3 p-4 text-left">
-
-
- {isTraining && }
-
-
-
- {isTraining ? "Training…" : hasModel ? "Model active" : "Auto-mode"}
-
-
- {isTraining
- ? "Celery worker is processing the task"
- : hasModel
- ? `Trained on ${modelInfo!.model!.points_count} pts · ${new Date(modelInfo!.model!.trained_at).toLocaleDateString("ru-RU")}`
- : "No manual model. Train on a clean period."}
-
-
-
-
-
- {expanded && (
-
- {hasModel && modelInfo?.model && (
-
-
Trained: {new Date(modelInfo.model.trained_at).toLocaleString("ru-RU")}
-
Period: {new Date(modelInfo.model.period_from).toLocaleString("ru-RU")} → {new Date(modelInfo.model.period_to).toLocaleString("ru-RU")}
- {modelInfo.model.note &&
Note: {modelInfo.model.note}
}
-
- )}
-
-
-
Train on a clean (normal load) period:
-
-
-
- {isTraining
- ?
- : }
- {phase.kind === "submitting" ? "Submitting…" : isTraining ? "Training…" : "Train model"}
-
- {hasModel && !isTraining && (
-
-
- {deleting ? "Deleting…" : "Delete model"}
-
- )}
-
-
-
-
-
- )}
-
- );
-}
-
-// ─── Main Dashboard ───────────────────────────────────────────────────────────
-export default function Dashboard() {
- const navigate = useNavigate();
- const [devices, setDevices] = useState([]);
- const [selectedDevices, setSelectedDevices] = useState([]);
- const [hours, setHours] = useState(1);
- const [data, setData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [lastFetched, setLastFetched] = useState(null);
- // Track whether initial URL read happened
- const initializedRef = useRef(false);
-
- // Stable color map — order determined by devices array from /devices
- const deviceColorMap: Record = {};
- devices.forEach((d, i) => { deviceColorMap[d.name] = getDeviceColor(i); });
-
- // ─── Selected devices → URL sync ─────────────────────────────────────────
- const handleSetSelected = useCallback((next: string[]) => {
- setSelectedDevices(next);
- setUrlDevices(next);
- }, []);
-
- // ─── Fetch devices list ───────────────────────────────────────────────────
- const fetchDevices = useCallback(async () => {
- try {
- const res = await apiFetch("/devices");
- const list: Device[] = await res.json();
- setDevices(list);
-
- if (!initializedRef.current) {
- initializedRef.current = true;
- const fromUrl = getUrlDevices();
- // Filter URL devices to only those that exist
- const valid = fromUrl.filter((n) => list.some((d) => d.name === n));
- if (valid.length > 0) {
- // URL had specific devices — use them and write back cleaned list
- setSelectedDevices(valid);
- setUrlDevices(valid);
- } else {
- // No URL param or none matched — select all, no URL param needed
- const all = list.map((d) => d.name);
- setSelectedDevices(all);
- setUrlDevices([]);
- }
- } else {
- // Subsequent refreshes: keep selection, just remove stale entries
- setSelectedDevices((prev) => {
- const kept = prev.filter((n) => list.some((d) => d.name === n));
- return kept.length > 0 ? kept : list.map((d) => d.name);
- });
- }
- } catch {}
- }, []);
-
- // ─── Fetch dashboard data (only from /api/dashboard) ─────────────────────
- const fetchDashboard = useCallback(async () => {
- try {
- const params = new URLSearchParams({ hours: String(hours) });
- const res = await apiFetch(`/api/dashboard?${params}`);
- const d: DashboardData = await res.json();
- setData(d);
- } catch {}
- }, [hours]);
-
- const fetchAll = useCallback(async () => {
- await fetchDashboard();
- setLastFetched(new Date());
- setLoading(false);
- }, [fetchDashboard]);
-
- useEffect(() => { fetchDevices(); }, [fetchDevices]);
-
- useEffect(() => {
- setLoading(true);
- fetchAll();
- const id = setInterval(fetchAll, 20000);
- return () => clearInterval(id);
- }, [fetchAll]);
-
- // ─── Derived data ─────────────────────────────────────────────────────────
- const filteredMetrics = (data?.metrics ?? []).filter((m) =>
- selectedDevices.includes(m.device)
- );
-
- const timelineMap: Record> = {};
- filteredMetrics.forEach((m) => {
- const t = formatTime(m.timestamp);
- if (!timelineMap[t]) timelineMap[t] = {};
- timelineMap[t][m.device] = { cpu: m.cpu, ram: m.ram };
- });
-
- const chartData = Object.entries(timelineMap)
- .sort(([a], [b]) => a.localeCompare(b))
- .map(([time, devs]) => ({
- time,
- ...Object.fromEntries(
- Object.entries(devs).flatMap(([dev, vals]) => [
- [`${dev}__cpu`, vals.cpu],
- [`${dev}__ram`, vals.ram],
- ])
- ),
- }));
-
- // Anomalies come from /api/dashboard — already have correct device field
- // Filter to selected devices only
- const filteredAnomalies = (data?.anomalies ?? []).filter((a) =>
- selectedDevices.includes(a.device)
- );
-
- const latestPerDevice: Record = {};
- filteredMetrics.forEach((m) => {
- if (!latestPerDevice[m.device] || m.timestamp > latestPerDevice[m.device].timestamp)
- latestPerDevice[m.device] = m;
- });
-
- const avgCpu = Object.values(latestPerDevice).length > 0
- ? Object.values(latestPerDevice).reduce((s, m) => s + m.cpu, 0) / Object.values(latestPerDevice).length
- : null;
-
- const avgRam = Object.values(latestPerDevice).length > 0
- ? Object.values(latestPerDevice).reduce((s, m) => s + m.ram, 0) / Object.values(latestPerDevice).length
- : null;
-
- const periodLabel = hours === 0 ? "all time" : hours < 24 ? `${hours}h` : `${hours / 24}d`;
-
- function AnomalyDots({ field }: { field: "cpu" | "ram" }) {
- return (
- <>
- {filteredAnomalies.map((a) => (
-
- ))}
- >
- );
- }
-
- return (
-
-
-
-
-
-
- {/* ── Top bar ── */}
-
- navigate("/admin")}
- title="Back to device registry"
- className="p-2 rounded-xl border border-slate-700/60 bg-slate-900/60 text-slate-400 hover:text-white hover:border-slate-600 transition-all"
- >
-
-
-
-
-
-
-
-
-
-
-
- {lastFetched && (
-
-
- {lastFetched.toLocaleTimeString()}
-
- )}
- { fetchDevices(); fetchAll(); }}
- className="p-2 rounded-xl border border-slate-700/60 bg-slate-900/60 text-slate-400 hover:text-white hover:border-slate-600 transition-all"
- >
-
-
-
-
- {/* ── Stats ── */}
-
- }
- accent={avgCpu != null && avgCpu > 80 ? "text-red-400" : avgCpu != null && avgCpu > 60 ? "text-amber-400" : "text-white"}
- />
- }
- accent={avgRam != null && avgRam > 85 ? "text-red-400" : avgRam != null && avgRam > 70 ? "text-amber-400" : "text-white"}
- />
- }
- accent={filteredAnomalies.length > 0 ? "text-amber-400" : "text-white"}
- />
- }
- />
-
-
- {/* ── Main grid ── */}
-
-
- {/* Left: charts */}
-
-
- {/* CPU */}
-
-
-
-
CPU usage
- {filteredAnomalies.length > 0 && (
-
-
- {filteredAnomalies.length} anomalies marked
-
- )}
- %
-
- {loading ? (
-
- ) : chartData.length === 0 ? (
-
No data
- ) : (
-
-
-
-
-
- } />
- String(v).replace("__cpu", "")} />
- {selectedDevices.map((dev) => (
-
- ))}
-
-
-
- )}
-
-
- {/* RAM */}
-
-
-
-
RAM usage
- %
-
- {loading ? (
-
- ) : chartData.length === 0 ? (
-
No data
- ) : (
-
-
-
-
-
- } />
- String(v).replace("__ram", "")} />
- {selectedDevices.map((dev) => (
-
- ))}
-
-
-
- )}
-
-
- {/* Per-device mini cards */}
- {Object.keys(latestPerDevice).length > 1 && (
-
-
Latest per device
-
- {Object.entries(latestPerDevice).map(([name, m]) => {
- const color = deviceColorMap[name];
- return (
-
-
-
- {name}
- {formatTime(m.timestamp)}
-
- {(["cpu", "ram"] as const).map((field) => (
-
-
- {field}
- {m[field].toFixed(1)}%
-
-
-
(field === "cpu" ? 80 : 85) ? "#f87171"
- : m[field] > (field === "cpu" ? 60 : 70) ? "#fb923c" : color,
- }}
- />
-
-
- ))}
-
- );
- })}
-
-
- )}
-
-
- {/* Right sidebar */}
-
-
-
-
-
-
-
Anomaly log
- {filteredAnomalies.length > 0 && (
-
- {filteredAnomalies.length}
-
- )}
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/DeviceAdmin.tsx b/frontend/src/DeviceAdmin.tsx
deleted file mode 100644
index 0a6deb6..0000000
--- a/frontend/src/DeviceAdmin.tsx
+++ /dev/null
@@ -1,594 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { useNavigate } from "react-router-dom";
-import { apiFetch } from "./api";
-import {
- Server,
- Plus,
- Activity,
- X,
- RefreshCw,
- ChevronRight,
- Wifi,
- WifiOff,
- Trash2,
-} from "lucide-react";
-
-// ─── Types ──────────────────────────────────────────────────────────────────
-interface Device {
- id: number;
- ip: string;
- name: string;
- is_active: boolean;
-}
-
-// ─── Validation (mirrors backend rules — see note below for why) ───────────
-// Device name is injected into a PromQL label matcher on the backend
-// (job="{name}"), so only a safe allow-list is accepted: letters, digits,
-// underscore and hyphen. This mirrors the backend's Pydantic validator.
-const DEVICE_NAME_RE = /^[a-zA-Z0-9_-]{1,15}$/;
-
-function isValidIp(ip: string): boolean {
- const v = ip.trim();
-
- const v4parts = v.split(".");
- if (v4parts.length === 4) {
- return v4parts.every(
- (p) => /^\d{1,3}$/.test(p) && Number(p) <= 255 && p === String(Number(p))
- );
- }
-
- // Lightweight IPv6 sanity check — the backend does the authoritative
- // check with Python's `ipaddress` module, this is just for fast feedback.
- return /^[0-9a-fA-F:]+$/.test(v) && v.includes(":");
-}
-
-interface FieldErrors {
- ip?: string;
- name?: string;
-}
-
-// ─── Pulse dot ──────────────────────────────────────────────────────────────
-const PulseDot = ({ active }: { active: boolean }) => (
-
- {active && (
-
- )}
-
-
-);
-
-// ─── Confirm Delete Modal ───────────────────────────────────────────────────
-interface ConfirmDeleteModalProps {
- device: Device;
- onClose: () => void;
- onConfirm: () => void;
-}
-function ConfirmDeleteModal({ device, onClose, onConfirm }: ConfirmDeleteModalProps) {
- return (
-
-
-
- {/* Header glow bar */}
-
-
-
-
-
-
-
Delete {device.name}?
-
- Are you sure you want to remove {device.ip} ? This action cannot be undone.
-
-
-
-
- Cancel
-
-
- Delete
-
-
-
-
-
- );
-}
-
-// ─── Add Device Modal ────────────────────────────────────────────────────────
-interface AddDeviceModalProps {
- onClose: () => void;
- onAdded: () => void;
-}
-function AddDeviceModal({ onClose, onAdded }: AddDeviceModalProps) {
- const [form, setForm] = useState({ ip: "", name: "", is_active: true });
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState("");
- const [fieldErrors, setFieldErrors] = useState
({});
-
- const validate = (): boolean => {
- const errs: FieldErrors = {};
- const name = form.name.trim();
- const ip = form.ip.trim();
-
- if (!name) {
- errs.name = "Name is required.";
- } else if (!DEVICE_NAME_RE.test(name)) {
- errs.name = "Only letters, digits, '_' and '-' are allowed (max 15 chars).";
- }
-
- if (!ip) {
- errs.ip = "IP address is required.";
- } else if (!isValidIp(ip)) {
- errs.ip = "Enter a valid IPv4 or IPv6 address.";
- }
-
- setFieldErrors(errs);
- return Object.keys(errs).length === 0;
- };
-
- const handleSubmit = async () => {
- if (!validate()) return;
-
- setLoading(true);
- setError("");
- try {
- const res = await apiFetch("/devices", {
- method: "POST",
- body: JSON.stringify(form),
- });
- if (!res.ok) {
- const d = await res.json().catch(() => ({}));
-
- // If the backend rejected the payload on validation grounds
- // (FastAPI/Pydantic 422), map the errors back onto the fields
- // instead of showing a generic message.
- if (Array.isArray(d.detail)) {
- const errs: FieldErrors = {};
- for (const item of d.detail) {
- const field = item.loc?.[item.loc.length - 1];
- if (field === "name" || field === "ip") {
- errs[field as "name" | "ip"] = item.msg;
- }
- }
- if (Object.keys(errs).length) {
- setFieldErrors(errs);
- return;
- }
- }
-
- throw new Error(typeof d.detail === "string" ? d.detail : `HTTP ${res.status}`);
- }
- onAdded();
- onClose();
- } catch (e: any) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
Add device
-
-
-
-
-
-
-
-
- IP address
-
-
{
- setForm((f) => ({ ...f, ip: e.target.value }));
- if (fieldErrors.ip) setFieldErrors((fe) => ({ ...fe, ip: undefined }));
- }}
- className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${
- fieldErrors.ip
- ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
- : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
- }`}
- />
- {fieldErrors.ip && (
-
{fieldErrors.ip}
- )}
-
-
-
- Device name
-
-
{
- setForm((f) => ({ ...f, name: e.target.value }));
- if (fieldErrors.name) setFieldErrors((fe) => ({ ...fe, name: undefined }));
- }}
- className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${
- fieldErrors.name
- ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
- : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
- }`}
- />
- {fieldErrors.name && (
-
{fieldErrors.name}
- )}
-
-
- Active
- setForm((f) => ({ ...f, is_active: !f.is_active }))}
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
- form.is_active ? "bg-cyan-500" : "bg-slate-700"
- }`}
- >
-
-
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- Cancel
-
-
- {loading ? "Adding…" : "Add device"}
-
-
-
-
-
- );
-}
-
-// ─── Device Card ─────────────────────────────────────────────────────────────
-interface DeviceCardProps {
- device: Device;
- index: number;
- onDelete: (device: Device) => void;
-}
-function DeviceCard({ device, index, onDelete }: DeviceCardProps) {
- const navigate = useNavigate();
- const handleClick = () => {
- navigate(`/dashboard?device=${encodeURIComponent(device.name)}`);
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {device.is_active ? "online" : "offline"}
-
-
-
-
-
- {device.name}
-
-
-
- {device.is_active ? (
-
- ) : (
-
- )}
-
- {device.ip}
-
-
-
-
-
-
-
-
-
-
{
- e.stopPropagation();
- onDelete(device);
- }}
- className="text-slate-600 hover:text-red-500 hover:bg-red-500/10 p-1.5 rounded-lg transition-all"
- title="Delete device"
- >
-
-
-
-
-
- );
-}
-
-// ─── Stats Bar ───────────────────────────────────────────────────────────────
-function StatsBar({ devices }: { devices: Device[] }) {
- const total = devices.length;
- const online = devices.filter((d) => d.is_active).length;
- const offline = total - online;
-
- return (
-
- {[
- { label: "Total", value: total, color: "text-white" },
- { label: "Online", value: online, color: "text-emerald-400" },
- { label: "Offline", value: offline, color: "text-slate-500" },
- ].map((s) => (
-
-
{s.value}
-
{s.label}
-
- ))}
-
- );
-}
-
-// ─── Main App ────────────────────────────────────────────────────────────────
-export default function DeviceAdmin() {
- const [devices, setDevices] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState("");
- const [showModal, setShowModal] = useState(false);
- const [filter, setFilter] = useState<"all" | "online" | "offline">("all");
- const [lastFetched, setLastFetched] = useState(null);
- const [deviceToDelete, setDeviceToDelete] = useState(null);
-
- const fetchDevices = useCallback(async () => {
- try {
- const res = await apiFetch("/devices");
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- const data = await res.json();
- setDevices(data);
- setLastFetched(new Date());
- setError("");
- } catch (e: any) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- }, []);
-
- const handleDeleteConfirm = async () => {
- if (!deviceToDelete) return;
- try {
- const res = await apiFetch(`/devices/${deviceToDelete.id}`, {
- method: "DELETE",
- });
- if (!res.ok) throw new Error("Failed to delete device");
-
- setDeviceToDelete(null);
- fetchDevices();
- } catch (e: any) {
- setError(e.message);
- }
- };
-
- useEffect(() => {
- fetchDevices();
- const interval = setInterval(fetchDevices, 15000);
- return () => clearInterval(interval);
- }, [fetchDevices]);
-
- const filtered = devices.filter((d) => {
- if (filter === "online") return d.is_active;
- if (filter === "offline") return !d.is_active;
- return true;
- });
-
- return (
-
-
-
-
-
-
- {/* ── Header ── */}
-
-
-
-
-
-
-
-
- Infrastructure
-
-
-
- Device Registry
-
-
- Manage monitored endpoints
-
-
-
-
- {lastFetched && (
-
- synced {lastFetched.toLocaleTimeString()}
-
- )}
-
-
-
-
setShowModal(true)}
- className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold text-sm transition-all shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30"
- >
-
- Add device
-
-
-
-
-
- {/* ── Stats ── */}
- {!loading && !error &&
}
-
- {/* ── Filter tabs ── */}
-
- {(["all", "online", "offline"] as const).map((f) => (
- setFilter(f)}
- className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-all capitalize ${
- filter === f
- ? "bg-slate-700 text-white shadow-sm"
- : "text-slate-500 hover:text-slate-300"
- }`}
- >
- {f}
-
- ))}
-
-
- {/* ── Content ── */}
- {loading ? (
-
- ) : error ? (
-
-
Failed to load devices
-
{error}
-
- Retry
-
-
- ) : filtered.length === 0 ? (
-
-
-
-
-
- {filter === "all" ? "No devices registered yet." : `No ${filter} devices.`}
-
- {filter === "all" && (
-
setShowModal(true)}
- className="mt-4 text-xs text-cyan-500 hover:text-cyan-400 underline underline-offset-2"
- >
- Add your first device →
-
- )}
-
- ) : (
-
- {filtered.map((d, i) => (
- setDeviceToDelete(device)}
- />
- ))}
-
- )}
-
-
- {/* ── Modal ── */}
- {showModal && (
-
setShowModal(false)}
- onAdded={fetchDevices}
- />
- )}
-
- {deviceToDelete && (
- setDeviceToDelete(null)}
- onConfirm={handleDeleteConfirm}
- />
- )}
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx
index c3dedf6..45825f0 100644
--- a/frontend/src/Login.tsx
+++ b/frontend/src/Login.tsx
@@ -1,14 +1,20 @@
import { useState } from "react";
import { Shield, Lock, User } from "lucide-react";
import { useNavigate, useLocation } from "react-router-dom";
+import { apiFetch } from "./api";
+import type { Login } from "./types/Types";
-const API_BASE = "http://localhost:30080";
+interface FieldErrors {
+ username?: string;
+ password?: string;
+}
export default function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/dashboard";
+ const [fieldErrors, setFieldErrors] = useState({});
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -16,34 +22,38 @@ export default function LoginPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
+ const validate = (): boolean => {
+ const errs: FieldErrors = {};
+ if (!username) {
+ errs.username = "Username is required"
+ }
+
+ if (!password) {
+ errs.password = "Password is required"
+ }
+ setFieldErrors(errs);
+ return Object.keys(errs).length === 0;
+ }
+
async function login() {
+ if (!validate()) return;
+
setLoading(true);
setError("");
try {
- const res = await fetch(`${API_BASE}/api/auth/login`, {
+ await apiFetch("/auth/login", {
method: "POST",
- credentials: "include",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
+ data: {
username,
password,
- }),
+ },
});
- const data = await res.json();
-
- if (!res.ok)
- throw new Error(data.detail);
-
navigate(from, { replace: true });
- }
- catch (e:any) {
+ } catch (e: any) {
setError(e.message);
- }
- finally {
+ } finally {
setLoading(false);
}
}
@@ -52,7 +62,6 @@ export default function LoginPage() {
{/* background grid */}
-
-
-
+
-
-
-
-
+
-
@@ -93,7 +93,6 @@ export default function LoginPage() {
-
-
Password
-
-
setPassword(e.target.value)}
- className="w-full bg-slate-800 border border-slate-700 rounded-xl pl-11 pr-4 py-3 focus:outline-none focus:border-cyan-500 transition"
+ onChange={(e) => {
+ setPassword(e.target.value);
+ }}
+ className={`w-full bg-slate-800 border rounded-xl pl-11 pr-4 py-3 focus:outline-none transition ${fieldErrors.password
+ ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
+ : "border-slate-700 focus:border-cyan-500 focus:ring-cyan-500/30"}`}
/>
-
-
+ {fieldErrors.password && (
+
{fieldErrors.password}
+ )}
{error && (
@@ -156,15 +162,10 @@ export default function LoginPage() {
>
{loading ? "Signing in..." : "Sign In"}
-
-
-
-
-
);
}
\ No newline at end of file
diff --git a/frontend/src/ProtectedRoute.tsx b/frontend/src/ProtectedRoute.tsx
index f4084b8..4aaea69 100644
--- a/frontend/src/ProtectedRoute.tsx
+++ b/frontend/src/ProtectedRoute.tsx
@@ -8,8 +8,8 @@ export default function ProtectedRoute({children}:{children:React.ReactNode}){
const location = useLocation();
useEffect(() => {
- apiFetch("/api/auth/me")
- .then(res => setOk(res.ok))
+ apiFetch("/auth/me")
+ .then(() => setOk(true))
.catch(() => setOk(false))
.finally(() => setLoading(false));
}, []);
diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx
index 2c89b39..79837a0 100644
--- a/frontend/src/api.tsx
+++ b/frontend/src/api.tsx
@@ -1,33 +1,61 @@
-const API = "http://localhost:30080";
+import axios, { type AxiosRequestConfig, AxiosError } from "axios";
+import { API, API_VERSION } from "./config/config";
-export async function apiFetch(
- endpoint: string,
- options: RequestInit = {}
-) {
- const response = await fetch(API + endpoint, {
- credentials: "include",
- headers: {
- "Content-Type": "application/json",
- ...(options.headers ?? {}),
- },
- ...options,
- });
-
- if (response.status === 401) {
- window.location.href = "/login";
- throw new Error("Unauthorized");
+export class ApiError extends Error {
+ public status?: number;
+
+ constructor(message: string, status?: number) {
+ super(message);
+ this.status = status;
}
+}
- if (!response.ok) {
- let message = `HTTP ${response.status}`;
+function extractErrorMessage(error: AxiosError): string {
+ const data = error.response?.data;
- try {
- const data = await response.json();
- message = data.detail ?? data.message ?? message;
- } catch {}
+ if (typeof data?.detail === "string") return data.detail;
+ if (typeof data?.detail?.message === "string") return data.detail.message;
+ if (typeof data?.message === "string") return data.message;
- throw new Error(message);
+ if (Array.isArray(data?.detail) && data.detail[0]?.msg) {
+ return data.detail[0].msg;
}
- return response;
+ return error.response
+ ? `HTTP ${error.response.status}`
+ : error.message ?? "Network error";
+}
+
+export async function apiFetch(
+ endpoint: string,
+ options: AxiosRequestConfig = {}
+): Promise {
+ try {
+ const response = await axios(API + API_VERSION + endpoint, {
+ withCredentials: true,
+ headers: {
+ "Content-Type": "application/json",
+ ...(options.headers ?? {}),
+ },
+ ...options,
+ });
+
+ return response.data as T;
+ } catch (err) {
+ const error = err as AxiosError;
+
+ const isLoginRequest = endpoint.includes("/auth/login");
+
+ if (error.response?.status === 401 && !isLoginRequest) {
+ window.location.href = "/login";
+ throw new Error("Unauthorized");
+ }
+
+ // throw new Error(extractErrorMessage(error));
+
+ throw new ApiError(
+ extractErrorMessage(error),
+ error.response?.status
+ );
+ }
}
\ No newline at end of file
diff --git a/frontend/src/components/anomalyFeed.tsx b/frontend/src/components/anomalyFeed.tsx
new file mode 100644
index 0000000..733b404
--- /dev/null
+++ b/frontend/src/components/anomalyFeed.tsx
@@ -0,0 +1,67 @@
+import { Activity, AlertTriangle } from "lucide-react";
+import { formatDateTime } from "../helpers/helpers";
+import { useEffect, useRef } from "react";
+import type { Anomaly } from "../types/Types";
+
+export function AnomalyFeed({ anomalies, deviceColorMap }: {
+ anomalies: Anomaly[];
+ deviceColorMap: Record;
+}) {
+ const scrollRef = useRef(null);
+
+ // Auto-scroll to top whenever anomalies change (newest are prepended)
+ useEffect(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = 0;
+ }
+ }, [anomalies]);
+
+ if (anomalies.length === 0) {
+ return (
+
+
+
No anomalies detected
+
+ );
+ }
+
+ // Already sorted newest-first from API (order_by desc), but reverse just in case
+ const sorted = [...anomalies].sort(
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
+ );
+
+ return (
+
+ {sorted.map((a) => {
+ const color = deviceColorMap[a.device] ?? "#94a3b8";
+ const severity = a.score < -0.15 ? "high" : a.score < -0.05 ? "mid" : "low";
+ const sevColor = severity === "high" ? "#f87171" : severity === "mid" ? "#fb923c" : "#facc15";
+ return (
+
+
+
+
+
+ {a.device}
+ {formatDateTime(a.timestamp)}
+
+
{a.reason}
+
+ CPU {a.cpu.toFixed(1)}%
+ RAM {a.ram.toFixed(1)}%
+
+ score {a.score.toFixed(4)}
+
+
+
+
+
+ );
+ })}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/deviceCard.tsx b/frontend/src/components/deviceCard.tsx
new file mode 100644
index 0000000..829853f
--- /dev/null
+++ b/frontend/src/components/deviceCard.tsx
@@ -0,0 +1,98 @@
+import { Activity, ChevronRight, Server, Trash2, Wifi, WifiOff } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import type { Device } from "../pages/DeviceAdmin";
+
+interface DeviceCardProps {
+ device: Device;
+ index: number;
+ onDelete: (device: Device) => void;
+}
+
+const PulseDot = ({ active }: { active: boolean }) => (
+
+ {active && (
+
+ )}
+
+
+);
+
+export function DeviceCard({ device, index, onDelete }: DeviceCardProps) {
+ const navigate = useNavigate();
+ const handleClick = () => {
+ navigate(`/dashboard?device=${encodeURIComponent(device.name)}`);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {device.is_active ? "online" : "offline"}
+
+
+
+
+
+ {device.name}
+
+
+
+ {device.is_active ? (
+
+ ) : (
+
+ )}
+
+ {device.ip}
+
+
+
+
+
+
+
+
+
+
{
+ e.stopPropagation();
+ onDelete(device);
+ }}
+ className="text-slate-600 hover:text-red-500 hover:bg-red-500/10 p-1.5 rounded-lg transition-all"
+ title="Delete device"
+ >
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/deviceSelector.tsx b/frontend/src/components/deviceSelector.tsx
new file mode 100644
index 0000000..a313054
--- /dev/null
+++ b/frontend/src/components/deviceSelector.tsx
@@ -0,0 +1,99 @@
+import { useEffect, useRef, useState } from "react";
+import type { Device } from "../pages/DeviceAdmin";
+import { CheckSquare, ChevronDown, Server, Square, Wifi, WifiOff } from "lucide-react";
+
+export function DeviceSelector({ devices, selected, onChange, deviceColorMap }: {
+ devices: Device[];
+ selected: string[];
+ onChange: (s: string[]) => void;
+ deviceColorMap: Record;
+}) {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const h = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
+ };
+ document.addEventListener("mousedown", h);
+ return () => document.removeEventListener("mousedown", h);
+ }, []);
+
+ const toggle = (name: string) => {
+ if (selected.includes(name)) {
+ if (selected.length === 1) return;
+ onChange(selected.filter((d) => d !== name));
+ } else {
+ onChange([...selected, name]);
+ }
+ };
+
+ const allSelected = devices.length > 0 && devices.every((d) => selected.includes(d.name));
+
+ const label = allSelected
+ ? "All devices"
+ : selected.length === 1
+ ? selected[0]
+ : `${selected.length} devices`;
+
+ return (
+
+
setOpen((o) => !o)}
+ className="flex items-center gap-2 px-3 py-2 rounded-xl border border-slate-700/60 bg-slate-900/80 text-sm text-slate-200 hover:border-slate-600 transition-all"
+ >
+
+ {label}
+
+
+
+ {open && (
+
+
+
+ {/* All toggle */}
+
+ allSelected
+ ? onChange(devices.length > 0 ? [devices[0].name] : [])
+ : onChange(devices.map((d) => d.name))
+ }
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-xs text-slate-400 hover:text-white hover:bg-slate-800 transition-all mb-1"
+ >
+ {allSelected
+ ?
+ : }
+ All devices
+
+
+ {devices.map((device) => {
+ const checked = selected.includes(device.name);
+ const color = deviceColorMap[device.name];
+ return (
+
toggle(device.name)}
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-sm hover:bg-slate-800 transition-all"
+ >
+ {checked
+ ?
+ : }
+
+
+ {device.name}
+
+ {device.is_active
+ ?
+ : }
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/modals/addDeviceModal.tsx b/frontend/src/components/modals/addDeviceModal.tsx
new file mode 100644
index 0000000..21cb8ae
--- /dev/null
+++ b/frontend/src/components/modals/addDeviceModal.tsx
@@ -0,0 +1,198 @@
+import { useState } from "react";
+import { apiFetch } from "../../api";
+import {
+ X,
+} from "lucide-react";
+interface AddDeviceModalProps {
+ onClose: () => void;
+ onAdded: () => void;
+}
+
+const DEVICE_NAME_RE = /^[a-zA-Z0-9_-]{1,15}$/;
+
+function isValidIp(ip: string): boolean {
+ const v = ip.trim();
+
+ const v4parts = v.split(".");
+ if (v4parts.length === 4) {
+ return v4parts.every(
+ (p) => /^\d{1,3}$/.test(p) && Number(p) <= 255 && p === String(Number(p))
+ );
+ }
+
+ // Lightweight IPv6 sanity check — the backend does the authoritative
+ // check with Python's `ipaddress` module, this is just for fast feedback.
+ return /^[0-9a-fA-F:]+$/.test(v) && v.includes(":");
+}
+
+interface FieldErrors {
+ ip?: string;
+ name?: string;
+}
+
+export function AddDeviceModal({ onClose, onAdded }: AddDeviceModalProps) {
+ const [form, setForm] = useState({ ip: "", name: "", is_active: true });
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [fieldErrors, setFieldErrors] = useState({});
+
+ const validate = (): boolean => {
+ const errs: FieldErrors = {};
+ const name = form.name.trim();
+ const ip = form.ip.trim();
+
+ if (!name) {
+ errs.name = "Name is required.";
+ } else if (!DEVICE_NAME_RE.test(name)) {
+ errs.name = "Only letters, digits, '_' and '-' are allowed (max 15 chars).";
+ }
+
+ if (!ip) {
+ errs.ip = "IP address is required.";
+ } else if (!isValidIp(ip)) {
+ errs.ip = "Enter a valid IPv4 or IPv6 address.";
+ }
+
+ setFieldErrors(errs);
+ return Object.keys(errs).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validate()) return;
+
+ setLoading(true);
+ setError("");
+ try {
+ await apiFetch("/devices", {
+ method: "POST",
+ data: form,
+ });
+ onAdded();
+ onClose();
+ } catch (e: any) {
+ const d = e.response?.data ?? {};
+
+ if (Array.isArray(d.detail)) {
+ const errs: FieldErrors = {};
+ for (const item of d.detail) {
+ const field = item.loc?.[item.loc.length - 1];
+ if (field === "name" || field === "ip") {
+ errs[field as "name" | "ip"] = item.msg;
+ }
+ }
+ if (Object.keys(errs).length) {
+ setFieldErrors(errs);
+ return;
+ }
+ }
+ setError(typeof d.detail === "string" ? d.detail : e.message ?? "Unknown error");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Add device
+
+
+
+
+
+
+
+
+ IP address
+
+
{
+ setForm((f) => ({ ...f, ip: e.target.value }));
+ if (fieldErrors.ip) setFieldErrors((fe) => ({ ...fe, ip: undefined }));
+ }}
+ className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${fieldErrors.ip
+ ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
+ : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
+ }`}
+ />
+ {fieldErrors.ip && (
+
{fieldErrors.ip}
+ )}
+
+
+
+ Device name
+
+
{
+ setForm((f) => ({ ...f, name: e.target.value }));
+ if (fieldErrors.name) setFieldErrors((fe) => ({ ...fe, name: undefined }));
+ }}
+ className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${fieldErrors.name
+ ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
+ : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
+ }`}
+ />
+ {fieldErrors.name && (
+
{fieldErrors.name}
+ )}
+
+
+ Active
+ setForm((f) => ({ ...f, is_active: !f.is_active }))}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors cursor-pointer focus:outline-none ${form.is_active ? "bg-cyan-500" : "bg-slate-700"
+ }`}
+ >
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ Cancel
+
+
+ {loading ? "Adding…" : "Add device"}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/modals/addUserModal.tsx b/frontend/src/components/modals/addUserModal.tsx
new file mode 100644
index 0000000..ce5d3c0
--- /dev/null
+++ b/frontend/src/components/modals/addUserModal.tsx
@@ -0,0 +1,186 @@
+import { useState } from "react";
+import { apiFetch } from "../../api";
+import {
+ X,
+} from "lucide-react";
+interface AddUserModalProps {
+ onClose: () => void;
+ fetchUsers: () => void;
+}
+
+const USER_NAME_RE = /^[a-zA-Z0-9_-]{1,15}$/;
+
+interface FieldErrors {
+ username?: string;
+ password?: string;
+}
+
+export function AddUserModal({ onClose, fetchUsers }: AddUserModalProps) {
+ const [form, setForm] = useState({ username: "", password: "", admin_privileges: false });
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [fieldErrors, setFieldErrors] = useState({});
+
+ const validate = (): boolean => {
+ const errs: FieldErrors = {};
+ const username = form.username.trim();
+ const password = form.password.trim();
+
+ if (!username) {
+ errs.username = "Name is required.";
+ } else if (!USER_NAME_RE.test(username)) {
+ errs.username = "Only letters, digits, '_' and '-' are allowed (max 15 chars).";
+ }
+
+ if (!password) {
+ errs.password = "password is required.";
+ }
+
+ setFieldErrors(errs);
+ return Object.keys(errs).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validate()) return;
+
+ setLoading(true);
+ setError("");
+ try {
+ const payload = {
+ username: form.username.trim(),
+ password: form.password.trim(),
+ role: form.admin_privileges ? "admin" : "user",
+ };
+ await apiFetch("/user", {
+ method: "POST",
+ data: payload,
+ });
+ fetchUsers();
+ onClose();
+ } catch (e: any) {
+ const d = e.response?.data ?? {};
+
+ if (Array.isArray(d.detail)) {
+ const errs: FieldErrors = {};
+ for (const item of d.detail) {
+ const field = item.loc?.[item.loc.length - 1];
+ if (field === "username" || field === "password") {
+ errs[field as "username" | "password"] = item.msg;
+ }
+ }
+ if (Object.keys(errs).length) {
+ setFieldErrors(errs);
+ return;
+ }
+ }
+ setError(typeof d.detail === "string" ? d.detail : e.message ?? "Unknown error");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Add user
+
+
+
+
+
+
+
+
+ Username
+
+
{
+ setForm((f) => ({ ...f, username: e.target.value }));
+ if (fieldErrors.username) setFieldErrors((fe) => ({ ...fe, username: undefined }));
+ }}
+ className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${fieldErrors.username
+ ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
+ : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
+ }`}
+ />
+ {fieldErrors.username && (
+
{fieldErrors.username}
+ )}
+
+
+
+ Password
+
+
{
+ setForm((f) => ({ ...f, password: e.target.value }));
+ if (fieldErrors.password) setFieldErrors((fe) => ({ ...fe, password: undefined }));
+ }}
+ className={`w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono ${fieldErrors.password
+ ? "border-red-500/70 focus:border-red-500 focus:ring-red-500/30"
+ : "border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30"
+ }`}
+ />
+ {fieldErrors.password && (
+
{fieldErrors.password}
+ )}
+
+
+ Admin Privileges
+ setForm((f) => ({ ...f, admin_privileges: !f.admin_privileges }))}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${form.admin_privileges ? "bg-cyan-500" : "bg-slate-700"
+ }`}
+ >
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ Cancel
+
+
+ {loading ? "Adding…" : "Add user"}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/modals/confirmDeleteModal.tsx b/frontend/src/components/modals/confirmDeleteModal.tsx
new file mode 100644
index 0000000..904ad3d
--- /dev/null
+++ b/frontend/src/components/modals/confirmDeleteModal.tsx
@@ -0,0 +1,44 @@
+import { Trash2 } from "lucide-react";
+import type { Device } from "../../pages/DeviceAdmin";
+
+interface ConfirmDeleteModalProps {
+ device: Device;
+ onClose: () => void;
+ onConfirm: () => void;
+}
+export function ConfirmDeleteModal({ device, onClose, onConfirm }: ConfirmDeleteModalProps) {
+ return (
+
+
+
+ {/* Header glow bar */}
+
+
+
+
+
+
+
Delete {device.name}?
+
+ Are you sure you want to remove {device.ip} ? This action cannot be undone.
+
+
+
+
+ Cancel
+
+
+ Delete
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/modals/userModal.tsx b/frontend/src/components/modals/userModal.tsx
new file mode 100644
index 0000000..22d7e32
--- /dev/null
+++ b/frontend/src/components/modals/userModal.tsx
@@ -0,0 +1,350 @@
+import { useEffect, useState } from "react";
+import { Check, User2, } from "lucide-react";
+import { apiFetch } from "../../api";
+import {
+ X,
+} from "lucide-react";
+import type { User } from "../../types/Types";
+
+interface UserModalProps {
+ user: User
+ onClose: () => void;
+ fetchUsers: () => void;
+}
+
+
+interface FieldErrors {
+ username?: string;
+ password?: string;
+}
+
+export function UserModal({ user, onClose, fetchUsers }: UserModalProps) {
+ const [loading, setLoading] = useState(false);
+ const [deleteError, setDeleteError] = useState("");
+ const [roleError, setRoleError] = useState("");
+ const [passwordError, setPasswordError] = useState("");
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
+ const [confirmingChangeRole, setConfirmingChangeRole] = useState(false);
+ const [modalNewUserPassword, setModalNewUserPassword] = useState(false);
+ const [newUserPassword, setNewUserPassword] = useState({ password: "" });
+
+ const validate_password = (): boolean => {
+ if (!newUserPassword.password) {
+ setPasswordError("Password is required");
+ return false;
+ }
+ return true;
+ };
+
+ const handleUserDelete = async () => {
+ setLoading(true);
+ setDeleteError("");
+ try {
+ await apiFetch(`/user/${user.id}`, {
+ method: "DELETE",
+ });
+ fetchUsers();
+ setConfirmingDelete(false);
+ onClose();
+ } catch (e: any) {
+ const d = e.response?.data ?? {};
+
+ if (Array.isArray(d.detail)) {
+ const errs: FieldErrors = {};
+ for (const item of d.detail) {
+ const field = item.loc?.[item.loc.length - 1];
+ if (field === "username" || field === "password") {
+ errs[field as "username" | "password"] = item.msg;
+ }
+ }
+
+ }
+ const message =
+ d.message ??
+ (typeof d.detail === "string" ? d.detail : d.detail?.message) ??
+ e.message ??
+ "Unknown error";
+ setDeleteError(message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUserChangeRole = async () => {
+ setLoading(true);
+ setRoleError("");
+ try {
+ const payload = {
+ role: user.role === "admin" ? "user" : "admin"
+ }
+ await apiFetch(`/user/${user.id}/role`, {
+ method: "PATCH",
+ data: payload,
+ });
+ fetchUsers();
+ setConfirmingChangeRole(false);
+ onClose();
+ } catch (e: any) {
+ const d = e.response?.data ?? {};
+
+ if (Array.isArray(d.detail)) {
+ const errs: FieldErrors = {};
+ for (const item of d.detail) {
+ const field = item.loc?.[item.loc.length - 1];
+ if (field === "username" || field === "password") {
+ errs[field as "username" | "password"] = item.msg;
+ }
+ }
+ }
+ const message =
+ d.message ??
+ (typeof d.detail === "string" ? d.detail : d.detail?.message) ??
+ e.message ??
+ "Unknown error";
+ setRoleError(message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUserChangePassword = async () => {
+ if (!validate_password()) return;
+
+ setLoading(true);
+ setPasswordError("");
+ try {
+ const payload = {
+ username: user.username,
+ new_password: newUserPassword.password,
+ };
+ await apiFetch("/user/change-password", {
+ method: "PATCH",
+ data: payload,
+ });
+ setModalNewUserPassword(false);
+ setNewUserPassword({ password: "" });
+ } catch (e: any) {
+ const d = e.response?.data ?? {};
+
+ if (Array.isArray(d.detail)) {
+ const errs: FieldErrors = {};
+ for (const item of d.detail) {
+ const field = item.loc?.[item.loc.length - 1];
+ if (field === "username" || field === "password") {
+ errs[field as "username" | "password"] = item.msg;
+ }
+ }
+ }
+ const message =
+ d.message ??
+ (typeof d.detail === "string" ? d.detail : d.detail?.message) ??
+ e.message ??
+ "Unknown error";
+ setPasswordError(message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ setConfirmingDelete(false);
+ setModalNewUserPassword(false);
+ setNewUserPassword({ password: "" });
+ setRoleError("");
+ setPasswordError("");
+ setDeleteError("");
+ }, [user]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Username: {user.username}
+
+
+ Role: {user.role}
+
+
+ Admin: {user.role === "admin" ? : }
+
+
+
+
+ {/* {roleError && (
+
+ {roleError}
+
+ )} */}
+
+
+
+ {/*
+ Change role
+ */}
+ {confirmingChangeRole ? (
+
+
+
Change role for this user?
+ {roleError && (
+
+ {roleError}
+
+ )}
+
+ setConfirmingChangeRole(false)}
+ className="flex-1 px-4 py-2.5 rounded-xl border border-cyan-600 text-cyan-600 text-sm hover:text-cyan-500 hover:border-cyan-500 transition-all"
+ >
+ Cancel
+
+
+ {loading ? "Changing role" : "Change role"}
+
+
+
+
+ ) : (
+
setConfirmingChangeRole(true)}
+ className="flex-1 px-4 py-2.5 rounded-xl border border-slate-700 text-slate-400 text-sm hover:text-white hover:border-slate-600 transition-all"
+ >
+ {loading ? "Changing role" : "Change role"}
+
+ )}
+ {/*
+ Change password
+ */}
+ {modalNewUserPassword ? (
+
+
+
Change password for this user
+
+ This action cannot be undone.
+
+ {passwordError && (
+
+ {passwordError}
+
+ )}
+
+ New password
+
+
{
+ setNewUserPassword((f) => ({ ...f, password: e.target.value }));
+ // if (fieldErrors.username) setFieldErrors((fe) => ({ ...fe, username: undefined }));
+ }}
+ className={"w-full bg-slate-800/60 border text-white placeholder-slate-500 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-1 transition-all font-mono border-slate-700/70 focus:border-cyan-500/70 focus:ring-cyan-500/30 mb-4"}
+ />
+
+ setModalNewUserPassword(false)}
+ className="flex-1 px-4 py-2.5 rounded-xl border border-cyan-600 text-cyan-600 text-sm hover:text-cyan-500 hover:border-cyan-500 transition-all gap-5"
+ >
+ Cancel
+
+
+ {loading ? "Changing" : "Change"}
+
+
+
+
+ ) : (
+
setModalNewUserPassword(true)}
+ className="flex-1 px-4 py-2.5 rounded-xl border border-slate-700 text-slate-400 text-sm hover:text-white hover:border-slate-600 transition-all"
+ >
+ {loading ? "Changing" : "Change password"}
+
+ )}
+
+
+
+ Cancel
+
+ {confirmingDelete ? (
+
+
+
Delete this user?
+
+ This action cannot be undone.
+
+ {deleteError && (
+
+ {deleteError}
+
+ )}
+
+ setConfirmingDelete(false)}
+ className="flex-1 px-4 py-2.5 rounded-xl border border-cyan-600 text-cyan-600 text-sm hover:text-cyan-500 hover:border-cyan-500 transition-all"
+ >
+ Cancel
+
+
+ {loading ? "Deleting…" : "Delete"}
+
+
+
+
+ ) : (
+
setConfirmingDelete(true)}
+ className="flex-1 px-4 py-2.5 rounded-xl bg-red-500/85 hover:bg-red-400/90 text-slate-950 font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-cyan-500/20"
+ >
+ {loading ? "Deleting…" : "Delete user"}
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/modelPanel.tsx b/frontend/src/components/modelPanel.tsx
new file mode 100644
index 0000000..957e86f
--- /dev/null
+++ b/frontend/src/components/modelPanel.tsx
@@ -0,0 +1,293 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { type TaskStatus, type ModelInfo, type TrainPhase, type TrainResponse, type TaskResultPayload } from "../types/Types";
+import { apiFetch } from "../api";
+import { AlertTriangle, BrainCircuit, ChevronDown, Trash2, X, Zap } from "lucide-react";
+
+export function ModelPanel({ onTrained }: { onTrained: () => void }) {
+ const [modelInfo, setModelInfo] = useState(null);
+ const [trainHours, setTrainHours] = useState("1");
+ const [trainNote, setTrainNote] = useState("");
+ const [phase, setPhase] = useState({ kind: "idle" });
+ const [deleting, setDeleting] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const pollRef = useRef | null>(null);
+
+ const fetchModelInfo = useCallback(async () => {
+ try {
+ const res = await apiFetch("/api/v1/model-info");
+ setModelInfo(res);
+ } catch { }
+ }, []);
+
+ useEffect(() => { fetchModelInfo(); }, [fetchModelInfo]);
+ useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []);
+
+ const startPolling = useCallback((taskId: string, prevPoints: number) => {
+ let elapsed = 0;
+ const INTERVAL = 2000; // poll every 2s
+ const TIMEOUT = 120000;
+
+ const stop = () => {
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
+ };
+
+ pollRef.current = setInterval(async () => {
+ elapsed += INTERVAL;
+ setPhase({ kind: "polling", taskId, elapsed });
+
+ // ── 1. Check Celery task result via /task-status ──────────────────────
+ try {
+ const ts = await apiFetch(`/task-status/${taskId}`);
+ // state: PENDING | STARTED | SUCCESS | FAILURE | RETRY | REVOKED
+ if (ts.state === "SUCCESS") {
+ const res = typeof ts.result === "object" && ts.result !== null
+ ? (ts.result as TaskResultPayload)
+ : {};
+
+ if (res.status === "error") {
+ stop();
+ setPhase({
+ kind: "error",
+ text: res.message ?? "Training failed"
+ });
+ return;
+ }
+
+ stop();
+ await fetchModelInfo();
+
+ const infoRes = await apiFetch("/model-info");
+ const info: ModelInfo = infoRes ?? { status: "no_model" };
+
+ setModelInfo(info);
+
+ const pts = info.status === "ok" ? info.model!.points_count : prevPoints;
+ setPhase({ kind: "success", text: `Model trained on ${pts} points` });
+
+ onTrained();
+ return;
+ }
+
+ if (ts.state === "FAILURE") {
+ stop();
+ const msg = typeof ts.result === "string" ? ts.result : JSON.stringify(ts.result);
+ setPhase({ kind: "error", text: `Worker error: ${msg}` });
+ return;
+ }
+
+ } catch {
+ // /task-status not available yet — fall through to model-info fallback
+ }
+
+ // ── 2. Fallback: if task-status endpoint missing, detect via model-info ─
+ try {
+ const info = await apiFetch("/model-info");
+ if (info.status === "ok" && (info.model?.points_count ?? 0) !== prevPoints) {
+ stop();
+ setModelInfo(info);
+ setPhase({ kind: "success", text: `Model trained on ${info.model!.points_count} points` });
+ onTrained();
+ return;
+ }
+
+ } catch { }
+
+ if (elapsed >= TIMEOUT) {
+ stop();
+ setPhase({ kind: "error", text: "Training timed out. Check Celery worker logs." });
+ }
+ }, INTERVAL);
+ }, [fetchModelInfo, onTrained]);
+
+ const handleTrain = async () => {
+ if (pollRef.current) return;
+ const prevPoints = modelInfo?.model?.points_count ?? 0;
+ setPhase({ kind: "submitting" });
+ try {
+ const params = new URLSearchParams({ hours: trainHours, note: trainNote });
+ const d = await apiFetch(`/train?${params}`, {
+ method: "POST",
+ });
+
+ if (d.status === "accepted") {
+ setPhase({ kind: "polling", taskId: d.task_id ?? "?", elapsed: 0 });
+ startPolling(d.task_id ?? "?", prevPoints);
+ } else if (d.status === "ok" || d.status === "success") {
+ setPhase({ kind: "success", text: d.message ?? "Model trained successfully" });
+ await fetchModelInfo();
+ onTrained();
+ } else {
+ setPhase({ kind: "error", text: d.message ?? "Unknown error from server" });
+ }
+ } catch (e: any) {
+ if (e.status === 403) {
+ setPhase({ kind: "error", text: "Training the model requires admin privileges" });
+ } else {
+ setPhase({ kind: "error", text: `error: ${e.message}`})
+ }
+
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!confirm("Reset model and return to auto-mode?")) return;
+ setDeleting(true);
+ try {
+ await apiFetch("/api/v1/model", {
+ method: "DELETE",
+ });
+ await fetchModelInfo();
+ setPhase({ kind: "success", text: "Model deleted. Switched to auto-mode." });
+ onTrained();
+ } catch (e: any) {
+ if (e.response?.status === 403) {
+ setPhase({ kind: "error", text: "Deleting the model requires admin privileges" });
+ } else {
+ const d = e.response?.data ?? {};
+ const detail = d.detail;
+ const message =
+ typeof detail === "string"
+ ? detail
+ : detail?.message ?? e.message ?? "Unknown error";
+ setPhase({ kind: "error", text: `Failed to delete: ${message}` });
+ }
+ } finally { setDeleting(false); }
+ };
+
+ const isTraining = phase.kind === "submitting" || phase.kind === "polling";
+ const hasModel = modelInfo?.status === "ok";
+
+ function StatusBanner() {
+ if (phase.kind === "idle") return null;
+
+ if (phase.kind === "submitting") return (
+
+
+ Submitting task to Celery…
+
+ );
+
+ if (phase.kind === "polling") {
+ const secs = Math.round(phase.elapsed / 1000);
+ return (
+
+
+
+
Training in background…
+
{secs}s
+
+
+ task_id: {phase.taskId.slice(0, 18)}…
+
+
+
+ );
+ }
+
+ if (phase.kind === "success") return (
+
+ ✓
+ {phase.text}
+ setPhase({ kind: "idle" })} className="ml-auto text-emerald-600 hover:text-emerald-400 transition-colors">
+
+
+
+ );
+
+ if (phase.kind === "error") return (
+
+
+
+
Training failed
+
setPhase({ kind: "idle" })} className="text-red-700 hover:text-red-400 transition-colors">
+
+
+
+
{phase.text}
+
+ );
+
+ return null;
+ }
+
+ return (
+
+
setExpanded((e) => !e)} className="w-full flex items-center gap-3 p-4 text-left">
+
+
+ {isTraining && }
+
+
+
+ {isTraining ? "Training…" : hasModel ? "Model active" : "Auto-mode"}
+
+
+ {isTraining
+ ? "Celery worker is processing the task"
+ : hasModel
+ ? `Trained on ${modelInfo!.model!.points_count} pts · ${new Date(modelInfo!.model!.trained_at).toLocaleDateString("ru-RU")}`
+ : "No manual model. Train on a clean period."}
+
+
+
+
+
+ {expanded && (
+
+ {hasModel && modelInfo?.model && (
+
+
Trained: {new Date(modelInfo.model.trained_at).toLocaleString("ru-RU")}
+
Period: {new Date(modelInfo.model.period_from).toLocaleString("ru-RU")} → {new Date(modelInfo.model.period_to).toLocaleString("ru-RU")}
+ {modelInfo.model.note &&
Note: {modelInfo.model.note}
}
+
+ )}
+
+
+
Train on a clean (normal load) period:
+
+
+
+ {isTraining
+ ?
+ : }
+ {phase.kind === "submitting" ? "Submitting…" : isTraining ? "Training…" : "Train model"}
+
+ {hasModel && !isTraining && (
+
+
+ {deleting ? "Deleting…" : "Delete model"}
+
+ )}
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/periodSelector.tsx b/frontend/src/components/periodSelector.tsx
new file mode 100644
index 0000000..c5cab3b
--- /dev/null
+++ b/frontend/src/components/periodSelector.tsx
@@ -0,0 +1,24 @@
+const PERIODS = [
+ { label: "1h", value: 1 },
+ { label: "6h", value: 6 },
+ { label: "24h", value: 24 },
+ { label: "7d", value: 168 },
+ { label: "All", value: 0 },
+];
+export function PeriodSelector({ value, onChange }: { value: number; onChange: (v: number) => void }) {
+ return (
+
+ {PERIODS.map((p) => (
+ onChange(p.value)}
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
+ value === p.value ? "bg-slate-700 text-white" : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ {p.label}
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/statCard.tsx b/frontend/src/components/statCard.tsx
new file mode 100644
index 0000000..356531b
--- /dev/null
+++ b/frontend/src/components/statCard.tsx
@@ -0,0 +1,14 @@
+export function StatCard({ label, value, sub, icon, accent = "text-white" }: {
+ label: string; value: string | number; sub?: string; icon: React.ReactNode; accent?: string;
+}) {
+ return (
+
+
+ {label}
+ {icon}
+
+
{value}
+ {sub &&
{sub}
}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/statsBar.tsx b/frontend/src/components/statsBar.tsx
new file mode 100644
index 0000000..6b11f0b
--- /dev/null
+++ b/frontend/src/components/statsBar.tsx
@@ -0,0 +1,25 @@
+import type { Device } from "../pages/DeviceAdmin";
+
+export function StatsBar({ devices }: { devices: Device[] }) {
+ const total = devices.length;
+ const online = devices.filter((d) => d.is_active).length;
+ const offline = total - online;
+
+ return (
+
+ {[
+ { label: "Total", value: total, color: "text-white" },
+ { label: "Online", value: online, color: "text-emerald-400" },
+ { label: "Offline", value: offline, color: "text-slate-500" },
+ ].map((s) => (
+
+
{s.value}
+
{s.label}
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/userCard.tsx b/frontend/src/components/userCard.tsx
new file mode 100644
index 0000000..5b92e35
--- /dev/null
+++ b/frontend/src/components/userCard.tsx
@@ -0,0 +1,35 @@
+import { User2, } from "lucide-react";
+import type { User } from "../types/Types";
+
+interface UserCardProps {
+ user: User;
+ index: number;
+ onClick: () => void;
+}
+
+
+export function UserCard({ user, index, onClick }: UserCardProps) {
+ return (
+
+
+
+
+
+
+ role: {user.role}
+
+
+
+
+
+ {user.username}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/config/config.ts b/frontend/src/config/config.ts
new file mode 100644
index 0000000..11a5cd1
--- /dev/null
+++ b/frontend/src/config/config.ts
@@ -0,0 +1,2 @@
+export const API = import.meta.env.VITE_API_URL || "http://localhost:30080";
+export const API_VERSION = import.meta.env.VITE_API_VER || "/api/v1";
\ No newline at end of file
diff --git a/frontend/src/helpers/chartToolTip.tsx b/frontend/src/helpers/chartToolTip.tsx
new file mode 100644
index 0000000..3597e00
--- /dev/null
+++ b/frontend/src/helpers/chartToolTip.tsx
@@ -0,0 +1,39 @@
+import { AlertTriangle } from "lucide-react";
+import { formatTime } from "./helpers";
+import type { Anomaly } from "../types/Types";
+
+export function ChartTooltip({ active, payload, label, anomalies }: {
+ active?: boolean; payload?: any[]; label?: string; anomalies: Anomaly[];
+}) {
+ if (!active || !payload?.length) return null;
+ const matching = anomalies.filter((a) => formatTime(a.timestamp) === label);
+ return (
+
+
{label}
+ {payload.map((e: any) => (
+
+
+
+ {String(e.name).replace(/__cpu|__ram/, "")}
+
+
+ {typeof e.value === "number" ? e.value.toFixed(1) : e.value}%
+
+
+ ))}
+ {matching.length > 0 && (
+
+ {matching.map((a) => (
+
+
+
+ {a.device}:
+ {a.reason}
+
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/helpers.ts b/frontend/src/helpers/helpers.ts
new file mode 100644
index 0000000..d3866c4
--- /dev/null
+++ b/frontend/src/helpers/helpers.ts
@@ -0,0 +1,38 @@
+const DEVICE_COLORS = [
+ "#22d3ee", "#a78bfa", "#fb923c", "#34d399",
+ "#f472b6", "#facc15", "#60a5fa", "#f87171",
+];
+export function getDeviceColor(i: number) { return DEVICE_COLORS[i % DEVICE_COLORS.length]; }
+
+// ─── URL helpers ──────────────────────────────────────────────────────────────
+export function getUrlDevices(): string[] {
+ const params = new URLSearchParams(window.location.search);
+ const raw = params.get("device");
+ if (!raw) return [];
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
+}
+
+export function setUrlDevices(devices: string[]) {
+ const params = new URLSearchParams(window.location.search);
+ if (devices.length === 0) {
+ params.delete("device");
+ } else {
+ params.set("device", devices.join(","));
+ }
+ const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
+ window.history.replaceState(null, "", newUrl);
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+export function formatTime(iso: string) {
+ // Append Z if no timezone info to treat as UTC and avoid local-shift
+ const s = /[Zz]|[+-]\d{2}:\d{2}$/.test(iso) ? iso : iso + "Z";
+ return new Date(s).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+}
+export function formatDateTime(iso: string) {
+ const s = /[Zz]|[+-]\d{2}:\d{2}$/.test(iso) ? iso : iso + "Z";
+ return new Date(s).toLocaleString("ru-RU", {
+ day: "2-digit", month: "2-digit",
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/toolTip.tsx b/frontend/src/helpers/toolTip.tsx
new file mode 100644
index 0000000..3597e00
--- /dev/null
+++ b/frontend/src/helpers/toolTip.tsx
@@ -0,0 +1,39 @@
+import { AlertTriangle } from "lucide-react";
+import { formatTime } from "./helpers";
+import type { Anomaly } from "../types/Types";
+
+export function ChartTooltip({ active, payload, label, anomalies }: {
+ active?: boolean; payload?: any[]; label?: string; anomalies: Anomaly[];
+}) {
+ if (!active || !payload?.length) return null;
+ const matching = anomalies.filter((a) => formatTime(a.timestamp) === label);
+ return (
+
+
{label}
+ {payload.map((e: any) => (
+
+
+
+ {String(e.name).replace(/__cpu|__ram/, "")}
+
+
+ {typeof e.value === "number" ? e.value.toFixed(1) : e.value}%
+
+
+ ))}
+ {matching.length > 0 && (
+
+ {matching.map((a) => (
+
+
+
+ {a.device}:
+ {a.reason}
+
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index d8509fb..097a71d 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,41 +1,65 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
-import DeviceAdmin from './DeviceAdmin'
-import Dashboard from './Dashboard'
+import DeviceAdmin from './pages/DeviceAdmin'
+import Dashboard from './pages/Dashboard'
+import UserBoard from './pages/UserBoard'
import './index.css'
import ProtectedRoute from './ProtectedRoute'
import Login from './Login'
createRoot(document.getElementById('root')!).render(
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
- ,
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ {/*
+ }
+ /> */}
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ {/*
+ }
+ /> */}
+
+
+
+ ,
)
\ No newline at end of file
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..f4f4db7
--- /dev/null
+++ b/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,386 @@
+import { useState, useEffect, useCallback, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import { apiFetch } from "../api";
+import {
+ LineChart, Line, XAxis, YAxis, CartesianGrid,
+ Tooltip, ResponsiveContainer, ReferenceDot, Legend,
+} from "recharts";
+import {
+ Activity, AlertTriangle, ArrowLeft,
+ Clock, Cpu, MemoryStick, RefreshCw,
+ Server,
+} from "lucide-react";
+import { formatTime, getDeviceColor, getUrlDevices, setUrlDevices } from "../helpers/helpers";
+import type { DashboardData, MetricPoint } from "../types/Types";
+import type { Device } from "./DeviceAdmin";
+import { DeviceSelector } from "../components/deviceSelector";
+import { PeriodSelector } from "../components/periodSelector";
+import { StatCard } from "../components/statCard";
+import { ChartTooltip } from "../helpers/toolTip";
+import { ModelPanel } from "../components/modelPanel";
+import { AnomalyFeed } from "../components/anomalyFeed";
+
+// ─── Main Dashboard ───────────────────────────────────────────────────────────
+export default function Dashboard() {
+ const navigate = useNavigate();
+ const [devices, setDevices] = useState([]);
+ const [selectedDevices, setSelectedDevices] = useState([]);
+ const [hours, setHours] = useState(1);
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [lastFetched, setLastFetched] = useState(null);
+ // Track whether initial URL read happened
+ const initializedRef = useRef(false);
+
+ // Stable color map — order determined by devices array from /devices
+ const deviceColorMap: Record = {};
+ devices.forEach((d, i) => { deviceColorMap[d.name] = getDeviceColor(i); });
+
+ // ─── Selected devices → URL sync ─────────────────────────────────────────
+ const handleSetSelected = useCallback((next: string[]) => {
+ setSelectedDevices(next);
+ setUrlDevices(next);
+ }, []);
+
+ // ─── Fetch devices list ───────────────────────────────────────────────────
+ const fetchDevices = useCallback(async () => {
+ try {
+ const list: Device[] = await apiFetch("/devices");
+ setDevices(list);
+
+ if (!initializedRef.current) {
+ initializedRef.current = true;
+ const fromUrl = getUrlDevices();
+ // Filter URL devices to only those that exist
+ const valid = fromUrl.filter((n) => list.some((d) => d.name === n));
+ if (valid.length > 0) {
+ // URL had specific devices — use them and write back cleaned list
+ setSelectedDevices(valid);
+ setUrlDevices(valid);
+ } else {
+ // No URL param or none matched — select all, no URL param needed
+ const all = list.map((d) => d.name);
+ setSelectedDevices(all);
+ setUrlDevices([]);
+ }
+ } else {
+ // Subsequent refreshes: keep selection, just remove stale entries
+ setSelectedDevices((prev) => {
+ const kept = prev.filter((n) => list.some((d) => d.name === n));
+ return kept.length > 0 ? kept : list.map((d) => d.name);
+ });
+ }
+ } catch { }
+ }, []);
+
+ // ─── Fetch dashboard data (only from /api/dashboard) ─────────────────────
+ const fetchDashboard = useCallback(async () => {
+ try {
+ const params = new URLSearchParams({ hours: String(hours) });
+ const d = await apiFetch(`/dashboard?${params}`);
+ setData(d);
+ } catch { }
+ }, [hours]);
+
+ const fetchAll = useCallback(async () => {
+ await fetchDashboard();
+ setLastFetched(new Date());
+ setLoading(false);
+ }, [fetchDashboard]);
+
+ useEffect(() => { fetchDevices(); }, [fetchDevices]);
+
+ useEffect(() => {
+ setLoading(true);
+ fetchAll();
+ const id = setInterval(fetchAll, 20000);
+ return () => clearInterval(id);
+ }, [fetchAll]);
+
+ // ─── Derived data ─────────────────────────────────────────────────────────
+ const filteredMetrics = (data?.metrics ?? []).filter((m) =>
+ selectedDevices.includes(m.device)
+ );
+
+ const timelineMap: Record> = {};
+ filteredMetrics.forEach((m) => {
+ const t = formatTime(m.timestamp);
+ if (!timelineMap[t]) timelineMap[t] = {};
+ timelineMap[t][m.device] = { cpu: m.cpu, ram: m.ram };
+ });
+
+ const chartData = Object.entries(timelineMap)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([time, devs]) => ({
+ time,
+ ...Object.fromEntries(
+ Object.entries(devs).flatMap(([dev, vals]) => [
+ [`${dev}__cpu`, vals.cpu],
+ [`${dev}__ram`, vals.ram],
+ ])
+ ),
+ }));
+
+ // Anomalies come from /api/dashboard — already have correct device field
+ // Filter to selected devices only
+ const filteredAnomalies = (data?.anomalies ?? []).filter((a) =>
+ selectedDevices.includes(a.device)
+ );
+
+ const latestPerDevice: Record = {};
+ filteredMetrics.forEach((m) => {
+ if (!latestPerDevice[m.device] || m.timestamp > latestPerDevice[m.device].timestamp)
+ latestPerDevice[m.device] = m;
+ });
+
+ const avgCpu = Object.values(latestPerDevice).length > 0
+ ? Object.values(latestPerDevice).reduce((s, m) => s + m.cpu, 0) / Object.values(latestPerDevice).length
+ : null;
+
+ const avgRam = Object.values(latestPerDevice).length > 0
+ ? Object.values(latestPerDevice).reduce((s, m) => s + m.ram, 0) / Object.values(latestPerDevice).length
+ : null;
+
+ const periodLabel = hours === 0 ? "all time" : hours < 24 ? `${hours}h` : `${hours / 24}d`;
+
+ function AnomalyDots({ field }: { field: "cpu" | "ram" }) {
+ return (
+ <>
+ {filteredAnomalies.map((a) => (
+
+ ))}
+ >
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {/* ── Top bar ── */}
+
+ navigate("/")}
+ title="Back to device registry"
+ className="p-2 rounded-xl border border-slate-700/60 bg-slate-900/60 text-slate-400 hover:text-white hover:border-slate-600 transition-all"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {lastFetched && (
+
+
+ {lastFetched.toLocaleTimeString()}
+
+ )}
+ { fetchDevices(); fetchAll(); }}
+ className="p-2 rounded-xl border border-slate-700/60 bg-slate-900/60 text-slate-400 hover:text-white hover:border-slate-600 transition-all"
+ >
+
+
+
+
+ {/* ── Stats ── */}
+
+ }
+ accent={avgCpu != null && avgCpu > 80 ? "text-red-400" : avgCpu != null && avgCpu > 60 ? "text-amber-400" : "text-white"}
+ />
+ }
+ accent={avgRam != null && avgRam > 85 ? "text-red-400" : avgRam != null && avgRam > 70 ? "text-amber-400" : "text-white"}
+ />
+ }
+ accent={filteredAnomalies.length > 0 ? "text-amber-400" : "text-white"}
+ />
+ }
+ />
+
+
+ {/* ── Main grid ── */}
+
+
+ {/* Left: charts */}
+
+
+ {/* CPU */}
+
+
+
+
CPU usage
+ {filteredAnomalies.length > 0 && (
+
+
+ {filteredAnomalies.length} anomalies marked
+
+ )}
+ %
+
+ {loading ? (
+
+ ) : chartData.length === 0 ? (
+
No data
+ ) : (
+
+
+
+
+
+ } />
+ String(v).replace("__cpu", "")} />
+ {selectedDevices.map((dev) => (
+
+ ))}
+
+
+
+ )}
+
+
+ {/* RAM */}
+
+
+
+
RAM usage
+ %
+
+ {loading ? (
+
+ ) : chartData.length === 0 ? (
+
No data
+ ) : (
+
+
+
+
+
+ } />
+ String(v).replace("__ram", "")} />
+ {selectedDevices.map((dev) => (
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Per-device mini cards */}
+ {Object.keys(latestPerDevice).length > 1 && (
+
+
Latest per device
+
+ {Object.entries(latestPerDevice).map(([name, m]) => {
+ const color = deviceColorMap[name];
+ return (
+
+
+
+ {name}
+ {formatTime(m.timestamp)}
+
+ {(["cpu", "ram"] as const).map((field) => (
+
+
+ {field}
+ {m[field].toFixed(1)}%
+
+
+
(field === "cpu" ? 80 : 85) ? "#f87171"
+ : m[field] > (field === "cpu" ? 60 : 70) ? "#fb923c" : color,
+ }}
+ />
+
+
+ ))}
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* Right sidebar */}
+
+
+
+
+
+
+
Anomaly log
+ {filteredAnomalies.length > 0 && (
+
+ {filteredAnomalies.length}
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/DeviceAdmin.tsx b/frontend/src/pages/DeviceAdmin.tsx
new file mode 100644
index 0000000..f017e68
--- /dev/null
+++ b/frontend/src/pages/DeviceAdmin.tsx
@@ -0,0 +1,245 @@
+import { useState, useEffect, useCallback } from "react";
+import { apiFetch } from "../api";
+import {
+ Server,
+ Plus,
+ RefreshCw,
+} from "lucide-react";
+import { AddDeviceModal } from "../components/modals/addDeviceModal";
+import { ConfirmDeleteModal } from "../components/modals/confirmDeleteModal";
+import { DeviceCard } from "../components/deviceCard";
+import { StatsBar } from "../components/statsBar";
+import { useNavigate } from "react-router-dom";
+// import { AddUserModal } from "../components/modals/addUserModal";
+
+// ─── Types ──────────────────────────────────────────────────────────────────
+export interface Device {
+ id: number;
+ ip: string;
+ name: string;
+ is_active: boolean;
+}
+
+// ─── Main App ────────────────────────────────────────────────────────────────
+export default function DeviceAdmin() {
+ const navigate = useNavigate();
+ const [devices, setDevices] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [showAddDeviceModal, setAddDeviceShowModal] = useState(false);
+ const [filter, setFilter] = useState<"all" | "online" | "offline">("all");
+ const [lastFetched, setLastFetched] = useState(null);
+ const [deviceToDelete, setDeviceToDelete] = useState(null);
+
+ const fetchDevices = useCallback(async () => {
+ try {
+ const data = await apiFetch("/devices");
+ setDevices(data);
+ setLastFetched(new Date());
+ setError("");
+ } catch (e: any) {
+ setError(e.message);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // const fetchUsers = useCallback(async () => {
+ // try {
+ // const data = await apiFetch("/api/v1//users");
+ // setLastFetched(new Date());
+ // setError("");
+ // } catch (e: any) {
+ // setError(e.message);
+ // } finally {
+ // setLoading(false);
+ // }
+ // }, []);
+
+ const handleDeleteConfirm = async () => {
+ if (!deviceToDelete) return;
+ try {
+ await apiFetch(`/devices/${deviceToDelete.id}`, {
+ method: "DELETE",
+ });
+
+ setDeviceToDelete(null);
+ fetchDevices();
+ } catch (e: any) {
+ setError(e.message);
+ }
+ };
+
+ useEffect(() => {
+ fetchDevices();
+ const interval = setInterval(fetchDevices, 15000);
+ return () => clearInterval(interval);
+ }, [fetchDevices]);
+
+ const filtered = devices.filter((d) => {
+ if (filter === "online") return d.is_active;
+ if (filter === "offline") return !d.is_active;
+ return true;
+ });
+
+ return (
+
+
+
+
+
+
+ {/* ── Header ── */}
+
+
+
+
+
+
+
+
+ Infrastructure
+
+
+
+ Device Registry
+
+
+ Manage monitored endpoints
+
+
+
+
+ {lastFetched && (
+
+ synced {lastFetched.toLocaleTimeString()}
+
+ )}
+
+
+
+
setAddDeviceShowModal(true)}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold text-sm transition-all shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30 cursor-pointer"
+ >
+
+ Add device
+
+
navigate("/userboard")}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold text-sm transition-all shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30 cursor-pointer"
+ >
+ Userboard
+
+
navigate("/dashboard")}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold text-sm transition-all shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30 cursor-pointer"
+ >
+ Dashboard
+
+
+
+
+
+ {/* ── Stats ── */}
+ {!loading && !error &&
}
+
+ {/* ── Filter tabs ── */}
+
+ {(["all", "online", "offline"] as const).map((f) => (
+ setFilter(f)}
+ className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-all capitalize ${filter === f
+ ? "bg-slate-700 text-white shadow-sm"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ {f}
+
+ ))}
+
+
+ {/* ── Content ── */}
+ {loading ? (
+
+ ) : error ? (
+
+
Failed to load devices
+
{error}
+
+ Retry
+
+
+ ) : filtered.length === 0 ? (
+
+
+
+
+
+ {filter === "all" ? "No devices registered yet." : `No ${filter} devices.`}
+
+ {filter === "all" && (
+
setAddDeviceShowModal(true)}
+ className="mt-4 text-xs text-cyan-500 hover:text-cyan-400 underline underline-offset-2"
+ >
+ Add your first device →
+
+ )}
+
+ ) : (
+
+ {filtered.map((d, i) => (
+ setDeviceToDelete(device)}
+ />
+ ))}
+
+ )}
+
+
+ {/* ── Modal ── */}
+ {showAddDeviceModal && (
+
setAddDeviceShowModal(false)}
+ onAdded={fetchDevices}
+ />
+ )}
+
+ {/* {showAddUserModal && (
+ setAddUserShowModal(false)}
+ // onAdded={fetchUsers}
+ />
+ )} */}
+
+ {deviceToDelete && (
+ setDeviceToDelete(null)}
+ onConfirm={handleDeleteConfirm}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/UserBoard.tsx b/frontend/src/pages/UserBoard.tsx
new file mode 100644
index 0000000..1251fad
--- /dev/null
+++ b/frontend/src/pages/UserBoard.tsx
@@ -0,0 +1,145 @@
+import { ArrowLeft, Cpu, Plus, } from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
+import { AddUserModal } from "../components/modals/addUserModal";
+import type { User } from "../types/Types";
+import { apiFetch } from "../api";
+import { UserCard } from "../components/userCard";
+import { UserModal } from "../components/modals/userModal";
+import { useNavigate } from "react-router-dom";
+
+export default function UserBoard() {
+ const [showAddUserModal, setAddUserShowModal] = useState(false);
+ const navigate = useNavigate();
+
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+ const [selectedUser, setSelectedUser] = useState(null);
+
+ const fetchUsers = useCallback(async () => {
+ try {
+ const data = await apiFetch("/users");
+ setUsers(data);
+ setError("");
+ } catch (e: any) {
+ if (e.status === 403) {
+ setError("This page requires admin privileges");
+ } else {
+ setError(e.message);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchUsers();
+ const interval = setInterval(fetchUsers, 15000);
+ return () => clearInterval(interval);
+ }, [fetchUsers]);
+
+ return (
+
+
+
+
+
+
+
+
navigate("/")}
+ title="Back to device registry"
+ className="self-start p-2 rounded-xl border border-slate-700/60 bg-slate-900/60 text-slate-400 hover:text-white hover:border-slate-600 transition-all cursor-pointer"
+ >
+
+
+
+
+
+
+
+
+ Users control
+
+
+
+ Users Registry
+
+
+ Manage Users
+
+
+
+
+
+
setAddUserShowModal(true)}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold text-sm transition-all shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30 cursor-pointer"
+ >
+
+ Add user
+
+
+
+
+ {/* Content */}
+ {
+ loading ? (
+
+ ) : error ? (
+
+
Failed to load devices
+
{error}
+
+ Retry
+
+
+ ) : (
+
+ {users.map((d, i) => (
+ setSelectedUser(d)}
+ />
+ ))}
+
+ )
+ }
+
+
+ {showAddUserModal && (
+
setAddUserShowModal(false)}
+ fetchUsers={() => fetchUsers()}
+ />
+ )
+ }
+
+ {
+ selectedUser && (
+
+ setSelectedUser(null)}
+ fetchUsers={() => fetchUsers()}
+ />
+
+ )
+ }
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/types/Types.ts b/frontend/src/types/Types.ts
new file mode 100644
index 0000000..06964b1
--- /dev/null
+++ b/frontend/src/types/Types.ts
@@ -0,0 +1,80 @@
+export interface Device {
+ id: number;
+ ip: string;
+ name: string;
+ is_active: boolean;
+}
+
+export interface MetricPoint {
+ timestamp: string;
+ cpu: number;
+ ram: number;
+ device: string;
+}
+
+export interface Anomaly {
+ id: number;
+ timestamp: string;
+ cpu: number;
+ ram: number;
+ reason: string;
+ score: number;
+ device: string;
+}
+
+export interface ModelInfo {
+ status: "ok" | "no_model";
+ message?: string;
+ model?: {
+ trained_at: string;
+ trained_by: string;
+ points_count: number;
+ period_from: string;
+ period_to: string;
+ note: string | null;
+ };
+}
+
+export interface DashboardData {
+ devices: string[];
+ metrics: MetricPoint[];
+ anomalies: Anomaly[];
+}
+
+export type TrainPhase =
+ | { kind: "idle" }
+ | { kind: "submitting" }
+ | { kind: "polling"; taskId: string; elapsed: number }
+ | { kind: "success"; text: string }
+ | { kind: "error"; text: string };
+
+type CeleryState = "PENDING" | "STARTED" | "SUCCESS" | "FAILURE" | "RETRY" | "REVOKED";
+
+export interface TaskResultPayload {
+ status?: "ok" | "error" | string;
+ message?: string;
+ [key: string]: unknown;
+}
+
+export interface TaskStatus {
+ task_id?: string;
+ state: CeleryState;
+ result?: TaskResultPayload | string | unknown;
+}
+
+export interface TrainResponse {
+ status: "accepted" | "ok" | "success" | "error" | string;
+ task_id?: string;
+ message?: string;
+}
+
+export interface User {
+ id: number;
+ username: string;
+ role: string;
+}
+
+export interface Login {
+ username: string;
+ password: string;
+}
\ No newline at end of file
diff --git a/mona-chart/templates/celery-beat-deployment.yaml b/mona-chart/templates/celery-beat-deployment.yaml
index e08236e..4bbe031 100644
--- a/mona-chart/templates/celery-beat-deployment.yaml
+++ b/mona-chart/templates/celery-beat-deployment.yaml
@@ -77,6 +77,8 @@ spec:
command:
{{- toYaml .Values.celeryBeat.command | nindent 12 }}
env:
+ - name: LOG_LEVEL
+ value: {{ .Values.celeryBeat.env.LOG_LEVEL | quote}}
- name: REDIS_URL
value: {{ include "mona.redisUrl" . | quote }}
- name: POSTGRES_USER
diff --git a/mona-chart/templates/celery-worker-deployment.yaml b/mona-chart/templates/celery-worker-deployment.yaml
index 0248a8f..938a69c 100644
--- a/mona-chart/templates/celery-worker-deployment.yaml
+++ b/mona-chart/templates/celery-worker-deployment.yaml
@@ -68,7 +68,8 @@ spec:
command:
{{- toYaml .Values.celeryWorker.command | nindent 12 }}
env:
-
+ - name: LOG_LEVEL
+ value: {{ .Values.celeryWorker.env.LOG_LEVEL | quote}}
- name: REDIS_URL
value: {{ include "mona.redisUrl" . | quote }}
- name: POSTGRES_USER
diff --git a/mona-chart/templates/fastapi-deployment.yaml b/mona-chart/templates/fastapi-deployment.yaml
index 1e9e85b..ba07e59 100644
--- a/mona-chart/templates/fastapi-deployment.yaml
+++ b/mona-chart/templates/fastapi-deployment.yaml
@@ -94,6 +94,8 @@ spec:
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
+ - name: LOG_LEVEL
+ value: {{ .Values.fastapi.env.LOG_LEVEL | quote}}
- name: POSTGRES_USER
value: {{ .Values.postgres.auth.user | quote }}
- name: POSTGRES_PASSWORD
diff --git a/mona-chart/templates/postgres-deployment.yaml b/mona-chart/templates/postgres-deployment.yaml
index bdf5648..5d3d029 100644
--- a/mona-chart/templates/postgres-deployment.yaml
+++ b/mona-chart/templates/postgres-deployment.yaml
@@ -18,25 +18,6 @@ spec:
automountServiceAccountToken: {{ .Values.postgres.automountServiceAccountToken }}
securityContext:
{{- toYaml .Values.postgres.podSecurityContext | nindent 8 }}
- # initContainers:
- # - name: fix-permissions
- # image: busybox:1.36
- # imagePullPolicy: Always
- # securityContext:
- # runAsUser: 0
- # runAsNonRoot: false
- # allowPrivilegeEscalation: false
- # readOnlyRootFilesystem: false
- # capabilities:
- # drop: ["ALL"]
- # # command: ["sh", "-c", "mkdir -p /var/lib/postgresql/data/pgdata && chown -R 999:999 /var/lib/postgresql/data"]
- # command: ["sh", "-c", "mkdir -p /var/lib/postgresql/data/pgdata && chown -R 10001:10001 /var/lib/postgresql/data"]
- # resources:
- # requests: { memory: "16Mi", cpu: "10m" }
- # limits: { memory: "32Mi", cpu: "50m" }
- # volumeMounts:
- # - name: pgdata
- # mountPath: /var/lib/postgresql/data
containers:
- name: postgres
image: "{{ .Values.images.postgres.repository }}:{{ .Values.images.postgres.tag }}"
@@ -62,12 +43,12 @@ spec:
readOnly: true
livenessProbe:
exec:
- command: ["pg_isready", "-U", "{{ .Values.postgres.auth.user }}"]
+ command: ["pg_isready", "-U", "{{ .Values.postgres.auth.user }}", "-d", "{{ .Values.postgres.auth.db }}"]
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
exec:
- command: ["pg_isready", "-U", "{{ .Values.postgres.auth.user }}"]
+ command: ["pg_isready", "-U", "{{ .Values.postgres.auth.user }}", "-d", "{{ .Values.postgres.auth.db }}"]
initialDelaySeconds: 5
periodSeconds: 10
resources:
diff --git a/mona-chart/values.yaml b/mona-chart/values.yaml
index 8b5da54..91d5262 100644
--- a/mona-chart/values.yaml
+++ b/mona-chart/values.yaml
@@ -44,6 +44,7 @@ fastapi:
replicas: 1
automountServiceAccountToken: false
env:
+ LOG_LEVEL: INFO
ADMIN_USERNAMES: ""
ADMIN_PASSWORDS: ""
USER_USERNAMES: ""
@@ -123,6 +124,8 @@ react:
celeryWorker:
replicas: 1
automountServiceAccountToken: false
+ env:
+ LOG_LEVEL: INFO
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
@@ -163,6 +166,8 @@ celeryWorker:
celeryBeat:
replicas: 1
automountServiceAccountToken: false
+ env:
+ LOG_LEVEL: INFO
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
@@ -358,7 +363,6 @@ kube-prometheus-stack:
type: NodePort
nodePort: 30091
prometheusSpec:
- # Отключаем привязку к helm-релизу
serviceMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelector: {}
# External node exporter for monitoring physical PC metrics.
diff --git a/mona_core/celery_conf.py b/mona_core/celery_conf.py
index 70e47a2..abf6776 100644
--- a/mona_core/celery_conf.py
+++ b/mona_core/celery_conf.py
@@ -2,9 +2,6 @@
from mona_core.config import settings
-app = Celery("mona")
-
-# redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
redis_url = settings.redis_url
app = Celery(
@@ -14,11 +11,9 @@
backend=redis_url,
)
-
-# database_url = os.getenv("DATABASE_URL", "postgresql://myuser:1234@postgres:5432/mydb")
-database_url = settings.postgres_url
-if not database_url.startswith("db+"):
- database_url = f"db+{database_url}"
+# database_url = settings.postgres_url
+# if not database_url.startswith("db+"):
+# database_url = f"db+{database_url}"
app.conf.update(timezone="UTC", worker_pool="solo")
diff --git a/mona_core/config.py b/mona_core/config.py
index 61d74d7..1868ae0 100644
--- a/mona_core/config.py
+++ b/mona_core/config.py
@@ -1,14 +1,20 @@
+from celery import Celery
from pydantic import computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict
+from redis.asyncio import from_url
class Settings(BaseSettings):
+ # logger
+ log_level: str = "INFO"
+
# api
- cors_origins: list[str] = ["http://localhost:30081"]
+ cors_origins: list[str] = ["http://localhost:30081", "http://localhost:5173"]
# Security
max_attempts: int = 5
lockout_seconds: int = 300
+ session_ttl: int = 43200
# ML
window: int = 500
@@ -38,7 +44,13 @@ def postgres_url(self) -> str:
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
- model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
+ model_config = SettingsConfigDict(
+ env_file=".env", env_file_encoding="utf-8", extra="ignore"
+ )
settings = Settings()
+
+redis_url = settings.redis_url
+redis_client = from_url(redis_url, decode_responses=True)
+celery_client = Celery("mona", broker=redis_url, backend=redis_url)
diff --git a/mona_core/db.py b/mona_core/db.py
index 41bb318..71120ca 100644
--- a/mona_core/db.py
+++ b/mona_core/db.py
@@ -1,5 +1,5 @@
from datetime import UTC, datetime
-from typing import Annotated
+from typing import Annotated, Literal
import bcrypt
from sqlalchemy import (
@@ -11,7 +11,6 @@
from mona_core.config import settings
-# DATABASE = os.getenv("DATABASE_URL", "postgresql://myuser:1234@localhost:5432/mydb")
DATABASE = settings.postgres_url
engine = create_engine(DATABASE)
@@ -80,7 +79,7 @@ class Users(Base):
id: Mapped[int_pk]
username: Mapped[str] = mapped_column(unique=True)
password_hash: Mapped[str]
- role: Mapped[str] = mapped_column(default="user")
+ role: Mapped[Literal["user", "admin"]] = mapped_column(default="user")
def set_password(self, password: str):
salt = bcrypt.gensalt()
diff --git a/mona_core/logger.py b/mona_core/logger.py
new file mode 100644
index 0000000..26d7480
--- /dev/null
+++ b/mona_core/logger.py
@@ -0,0 +1,94 @@
+import json
+import logging
+import logging.config
+import sys
+from datetime import UTC, datetime
+from typing import Any
+
+from mona_core.config import settings
+
+_RESERVED_RECORD_ATTRS = frozenset(
+ logging.LogRecord("", 0, "", 0, "", None, None).__dict__.keys()
+)
+
+
+class JsonFormatter(logging.Formatter):
+ """Compact JSON formatter: one line per log entry, readable raw in
+ `docker logs`, still valid JSON for Loki (`| json | level="ERROR"`)."""
+
+ def format(self, record: logging.LogRecord) -> str:
+ message = record.getMessage()
+ if record.exc_info:
+ # flatten traceback into the message itself instead of a
+ # separate multi-line field — keeps one log line = one event
+ trace = self.formatException(record.exc_info).replace("\n", " | ")
+ message = f"{message} | {trace}"
+
+ payload: dict[str, Any] = {
+ "timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(
+ timespec="seconds"
+ ),
+ "level": record.levelname,
+ "logger": record.name,
+ "message": message,
+ }
+
+ # allow ad-hoc structured fields via logger.info(..., extra={...})
+ for key, value in record.__dict__.items():
+ if key not in _RESERVED_RECORD_ATTRS and key not in payload:
+ payload[key] = value
+
+ return json.dumps(payload, default=str)
+
+
+def configure_logging(level: str = "INFO") -> None:
+ """Configure root logging to emit JSON lines to stdout.
+
+ Called once at import time below. Safe to call again (e.g. in tests).
+ """
+ logging.config.dictConfig(
+ {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "formatters": {
+ "json": {"()": JsonFormatter},
+ },
+ "handlers": {
+ "stdout": {
+ "class": "logging.StreamHandler",
+ "stream": sys.stdout,
+ "formatter": "json",
+ },
+ },
+ "root": {
+ "handlers": ["stdout"],
+ "level": level.upper(),
+ },
+ "loggers": {
+ # keep uvicorn/celery noise readable but routed to the same handler
+ "uvicorn": {
+ "handlers": ["stdout"],
+ "level": level.upper(),
+ "propagate": False,
+ },
+ "uvicorn.error": {
+ "handlers": ["stdout"],
+ "level": level.upper(),
+ "propagate": False,
+ },
+ "uvicorn.access": {
+ "handlers": ["stdout"],
+ "level": level.upper(),
+ "propagate": False,
+ },
+ "celery": {
+ "handlers": ["stdout"],
+ "level": level.upper(),
+ "propagate": False,
+ },
+ },
+ }
+ )
+
+
+configure_logging(settings.log_level)
diff --git a/mona_core/main.py b/mona_core/main.py
index 0f5e965..8ca7ca0 100644
--- a/mona_core/main.py
+++ b/mona_core/main.py
@@ -1,30 +1,24 @@
from contextlib import asynccontextmanager
-from datetime import UTC, datetime, timedelta
-from celery import Celery
-from fastapi import Depends, FastAPI, HTTPException, Query
+from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
-from prometheus_client.core import REGISTRY, GaugeMetricFamily
+from fastapi.responses import JSONResponse
from prometheus_fastapi_instrumentator import Instrumentator
-from sqlalchemy import delete, select, text
-from sqlalchemy.orm import Session
from mona_core.config import settings
-from mona_core.db import Anomaly, Device, Metric, SessionLocal, TrainedModel
+from mona_core.logger import configure_logging
from mona_core.security import (
admin_router,
auth_router,
- get_db,
seed_admin,
user_router,
)
-from mona_core.validators import DeviceCreate
@asynccontextmanager
async def lifespan(app: FastAPI):
- # Base.metadata.create_all(engine)
seed_admin()
+ configure_logging()
yield
@@ -38,355 +32,34 @@ async def lifespan(app: FastAPI):
allow_headers=["*"],
)
-# redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
-redis_url = settings.redis_url
-celery_client = Celery("mona", broker=redis_url, backend=redis_url)
-Instrumentator().instrument(app).expose(app, endpoint="/metrics")
-
-
-# ─── API endpoints ──────────────────────────────────────────────────────────
-# ─── Probes (Liveness & Readiness) ──────────────────────────────────────────
-@app.get("/health/live", tags=["Health"])
-def liveness_probe():
- return {"status": "alive"}
-
-
-@app.get("/health/ready", tags=["Health"])
-def readiness_probe(db: Session = Depends(get_db)):
- try:
- db.execute(text("SELECT 1"))
- return {"status": "ready"}
- except Exception:
- raise HTTPException(status_code=503, detail="Database unavailable")
-
-
-# Alerts
-class AnomalyCollector(object):
- def collect(self):
- gauge = GaugeMetricFamily(
- "mona_anomaly_active",
- "Active ML anomalies detected in the last 3 minutes",
- labels=["device", "reason"],
- )
-
- db = SessionLocal()
- try:
- cutoff = datetime.now(UTC) - timedelta(minutes=0.2)
- recent_anomalies = (
- db.query(Anomaly)
- .filter(Anomaly.timestamp >= cutoff.replace(tzinfo=None))
- .all()
- )
-
- seen = set()
- for a in recent_anomalies:
- label_values = (a.device, a.reason)
- if label_values not in seen:
- gauge.add_metric([a.device, a.reason], 1)
- seen.add(label_values)
- except Exception as e:
- print(f"Error in Prometheus AnomalyCollector: {e}")
- finally:
- db.close()
-
- yield gauge
-
-
-try:
- REGISTRY.register(AnomalyCollector())
-except ValueError:
- pass
-
-# ─── Devices ────────────────────────────────────────────────────────────────
-
-
-@user_router.get("/devices")
-def list_devices(db: Session = Depends(get_db)):
- stmt = select(Device)
- return db.execute(stmt).scalars().all()
-
-
-@admin_router.post("/devices", status_code=201)
-def create_device(body: DeviceCreate, db: Session = Depends(get_db)):
- dev = Device(ip=body.ip, name=body.name, is_active=body.is_active)
- db.add(dev)
- try:
- db.commit()
- except Exception:
- db.rollback()
- raise HTTPException(status_code=409, detail="Name already exists")
- db.refresh(dev)
- return dev
-
-
-@admin_router.delete("/devices/{device_id}")
-def delete_device(device_id: int, db: Session = Depends(get_db)):
- stmt = delete(Device).where(Device.id == device_id)
- try:
- result = db.execute(stmt)
-
- if result.rowcount == 0: # type: ignore
- raise HTTPException(status_code=404, detail="Device not found")
-
- db.commit()
-
- return {"message": "Device deleted successfully"}
- except HTTPException:
- raise
- except Exception as e:
- db.rollback()
- raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
-
-
-@app.get("/api/prometheus/targets")
-def get_prometheus_targets(db: Session = Depends(get_db)):
- stmt = select(Device).where(Device.is_active, Device.ip.is_not(None))
- devices = db.execute(stmt).scalars().all()
-
- targets = []
- for dev in devices:
- targets.append(
- {
- "targets": [f"{dev.ip}:9100"],
- "labels": {
- "job": dev.name,
- "physical_pc": "true",
- "device_label": dev.name,
- },
- }
- )
- return targets
-
-
-# ─── Model ──────────────────────────────────────────────────────────────────
-@user_router.get("/anomalies")
-def get_anomalies(
- hours: int = 24,
- device: str | None = None,
- limit: int = Query(default=100, le=1000),
- offset: int = Query(default=0, ge=0),
- db: Session = Depends(get_db),
-):
- stmt = select(Anomaly)
- if device:
- stmt = stmt.where(Anomaly.device == device)
- if hours > 0:
- stmt = stmt.where(
- Anomaly.timestamp >= datetime.now(UTC) - timedelta(hours=hours)
- )
-
- stmt = stmt.order_by(Anomaly.timestamp.desc()).limit(limit).offset(offset)
- anomalies = db.execute(stmt).scalars().all()
- return {
- "items": [
- {
- "id": a.id,
- "metric_id": a.metric_id,
- "cpu": a.cpu,
- "ram": a.ram,
- "timestamp": a.timestamp,
- "reason": a.reason,
- "score": a.score,
- "detected_at": a.detected_at,
- "device": a.device,
- }
- for a in anomalies
- ],
- "limit": limit,
- "offset": offset,
- }
-
-
-@user_router.get("/model-info")
-def model_info(db: Session = Depends(get_db)):
- stmt = (
- select(TrainedModel)
- .where(TrainedModel.trained_by == "user")
- .order_by(TrainedModel.trained_at.desc())
- .limit(1)
- )
-
- record = db.execute(stmt).scalar_one_or_none()
-
- if record is None:
- return {
- "status": "no_model",
- "message": "Model is not manually trained yet. Using auto-mode.",
- }
- return {
- "status": "ok",
- "model": {
- "trained_at": record.trained_at,
- "trained_by": record.trained_by,
- "points_count": record.points_count,
- "period_from": record.period_from,
- "period_to": record.period_to,
- "note": record.note,
- },
- }
-
-
-@admin_router.post("/train", status_code=202)
-def train_model(
- hours: float = Query(
- default=1.0,
- ge=0,
- le=24 * 7,
- description="Hours of recent data to use for training",
- ),
- note: str = Query(default="", description="Comment (optional)"),
-):
-
- task = celery_client.send_task(
- "tasks.train_model_task", kwargs={"hours": hours, "note": note}
+@app.exception_handler(HTTPException)
+async def http_exception_handler(request: Request, exc: HTTPException):
+ detail = exc.detail
+ message = (
+ detail.get("message", str(detail)) if isinstance(detail, dict) else str(detail)
)
-
- return {
- "status": "accepted",
- "message": "Model training task has been submitted to the background.",
- "task_id": task.id,
- }
-
-
-@admin_router.delete("/model")
-def delete_model(db: Session = Depends(get_db)):
- """Deletes the custom model — Celery will return to auto-mode."""
- try:
- stmt = delete(TrainedModel).where(TrainedModel.trained_by == "user")
- result = db.execute(stmt)
- db.commit()
- return {
- "status": "ok",
- "deleted": result.rowcount, # type: ignore
- "message": "Model deleted. Celery switched to auto-mode.",
- }
- except Exception as e:
- db.rollback()
- raise HTTPException(status_code=500, detail=f"Training error: {str(e)}")
-
-
-# ─── Dashboard ──────────────────────────────────────────────────────────────
-@user_router.get("/api/dashboard")
-def get_dashboard_data(
- hours: int = 1,
- device: str | None = None,
- max_points: int = Query(default=2000, le=10000),
- db: Session = Depends(get_db),
-):
- devices_stmt = select(Metric.device).distinct()
- devices = db.execute(devices_stmt).scalars().all()
-
- since = datetime.now(UTC) - timedelta(hours=hours) if hours > 0 else None
-
- stmt_m = select(Metric)
- stmt_a = select(Anomaly)
-
- if device:
- stmt_m = stmt_m.where(Metric.device == device)
- stmt_a = stmt_a.where(Anomaly.device == device)
- if since:
- stmt_m = stmt_m.where(Metric.timestamp >= since)
- stmt_a = stmt_a.where(Anomaly.timestamp >= since)
-
- stmt_m = stmt_m.order_by(Metric.timestamp.desc()).limit(max_points)
- stmt_a = stmt_a.order_by(Anomaly.timestamp.desc()).limit(max_points)
-
- metrics = list(reversed(db.execute(stmt_m).scalars().all()))
- anomalies = list(reversed(db.execute(stmt_a).scalars().all()))
-
- model_stmt = (
- select(TrainedModel)
- .where(TrainedModel.trained_by == "user")
- .order_by(TrainedModel.trained_at.desc())
- .limit(1)
+ return JSONResponse(
+ status_code=exc.status_code,
+ content={"status": "error", "message": message},
)
- model_record = db.execute(model_stmt).scalar_one_or_none()
-
- model_info = None
- if model_record:
- model_info = {
- "trained_at": model_record.trained_at.isoformat(),
- "points_count": model_record.points_count,
- "period_from": model_record.period_from.isoformat()
- if model_record.period_from
- else None,
- "period_to": model_record.period_to.isoformat()
- if model_record.period_to
- else None,
- "note": model_record.note,
- }
-
- return {
- "devices": devices,
- "model": model_info,
- "metrics": [
- {
- "timestamp": m.timestamp.isoformat(),
- "cpu": m.cpu,
- "ram": m.ram,
- "device": m.device,
- }
- for m in metrics
- ],
- "anomalies": [
- {
- "id": a.id,
- "timestamp": a.timestamp.isoformat(),
- "cpu": a.cpu,
- "ram": a.ram,
- "reason": a.reason,
- "score": a.score,
- "device": a.device,
- }
- for a in anomalies
- ],
- }
-# ─── Metrics ────────────────────────────────────────────────────────────────
-@user_router.get("/db-metrics")
-def get_metrics(
- device: str | None = None,
- hours: int = Query(default=1, le=24 * 7),
- before: datetime | None = None,
- limit: int = Query(default=500, le=5000),
- db: Session = Depends(get_db),
-):
- since = datetime.now(UTC) - timedelta(hours=hours)
- stmt = select(Metric).where(Metric.timestamp >= since)
- if device:
- stmt = stmt.where(Metric.device == device)
- if before:
- stmt = stmt.where(Metric.timestamp < before)
- stmt = stmt.order_by(Metric.timestamp.desc()).limit(limit)
- rows = db.execute(stmt).scalars().all()
-
- next_cursor = rows[-1].timestamp if len(rows) == limit else None
- return {"items": rows, "next_cursor": next_cursor}
-
-
-# ─── Tasks ──────────────────────────────────────────────────────────────────
-@user_router.get("/task-status/{task_id}")
-def get_task_status(task_id: str):
- result = celery_client.AsyncResult(task_id)
-
- raw = None
- if result.ready():
- try:
- raw = result.result
- if isinstance(raw, Exception):
- raw = {"error": str(raw)}
- except Exception:
- raw = None
-
- return {
- "task_id": task_id,
- "state": result.state,
- "result": result.result if result.ready() else None,
- }
+Instrumentator().instrument(app).expose(app, endpoint="/metrics", tags=["Prometheus"])
+# ─── API endpoints ──────────────────────────────────────────────────────────
+from mona_core.routers import ( # noqa: F401 E402
+ dashboard,
+ devices,
+ health,
+ model,
+ monitoring,
+ tasks,
+ users,
+)
-app.include_router(admin_router)
-app.include_router(user_router)
-app.include_router(auth_router)
+app.include_router(monitoring.router_prometheus)
+app.include_router(health.router)
+app.include_router(admin_router, prefix="/api/v1", tags=["Admin"])
+app.include_router(user_router, prefix="/api/v1", tags=["User"])
+app.include_router(auth_router, prefix="/api/v1")
diff --git a/mona_core/ml.py b/mona_core/ml.py
index 32a7346..7a5dc62 100644
--- a/mona_core/ml.py
+++ b/mona_core/ml.py
@@ -13,6 +13,7 @@
"""
# import pickle
+import logging
from datetime import UTC, datetime, timedelta
import numpy as np
@@ -25,10 +26,8 @@
from mona_core.config import settings
from mona_core.db import Anomaly, Metric, SessionLocal, TrainedModel
-# WINDOW = 500
-# MIN_POINTS = 30
-# CONTAMINATION = 0.05
-# SCORE_THRESHOLD = -0.05
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
# ─── Helpers ────────────────────────────────────────────────────────────────
@@ -101,7 +100,6 @@ def _load_user_model(db):
return None, None
untrusted_types = sio.get_untrusted_types(data=record.model_data)
model, scaler = sio.loads(record.model_data, trusted=untrusted_types)
- # model, scaler = sio.loads(record.model_data, trusted=True) # type: ignore
return model, scaler
@@ -150,6 +148,7 @@ def detect_anomalies():
preds = user_model.predict(X_scaled)
scores = user_model.decision_function(X_scaled)
mode = "user_model"
+ logger.info("Model switched to user model")
else:
# ── Mode 2: on-the-fly training per device (fallback) ──
scaler = StandardScaler()
@@ -162,6 +161,7 @@ def detect_anomalies():
preds = model.fit_predict(X_scaled)
scores = model.decision_function(X_scaled)
mode = "auto"
+ logger.info("Model switched to mode auto")
for i, row in enumerate(rows):
if mode == "auto" and row.timestamp < cutoff_new.replace(tzinfo=None):
@@ -195,6 +195,7 @@ def detect_anomalies():
if new_anomalies:
db.add_all(new_anomalies)
db.commit()
+ logger.info("Found anomalies", extra={"Anomalies": new_anomalies})
return {
"status": "ok",
@@ -203,8 +204,9 @@ def detect_anomalies():
"anomalies_found": len(new_anomalies),
}
- except Exception as e:
+ except Exception:
db.rollback()
- return {"status": "error", "error": str(e)}
+ logger.exception("Server error during detect anomalies")
+ return {"status": "error", "error": "Server error"}
finally:
db.close()
diff --git a/mona_core/routers/__init__.py b/mona_core/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mona_core/routers/dashboard.py b/mona_core/routers/dashboard.py
new file mode 100644
index 0000000..162074e
--- /dev/null
+++ b/mona_core/routers/dashboard.py
@@ -0,0 +1,55 @@
+from datetime import UTC, datetime, timedelta
+
+from fastapi import Depends, Query
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from mona_core.db import Anomaly, Metric, TrainedModel
+from mona_core.schemas import DashboardAnomaly, DashboardMetric, DashboardOut, ModelInfo
+from mona_core.security import (
+ get_db,
+ user_router,
+)
+
+
+@user_router.get("/dashboard", response_model=DashboardOut)
+def get_dashboard_data(
+ hours: int = 1,
+ device: str | None = None,
+ max_points: int = Query(default=2000, le=10000),
+ db: Session = Depends(get_db),
+) -> DashboardOut:
+ devices = list(db.scalars(select(Metric.device).distinct()).all())
+
+ since = datetime.now(UTC) - timedelta(hours=hours) if hours > 0 else None
+
+ stmt_m = select(Metric)
+ stmt_a = select(Anomaly)
+
+ if device:
+ stmt_m = stmt_m.where(Metric.device == device)
+ stmt_a = stmt_a.where(Anomaly.device == device)
+ if since:
+ stmt_m = stmt_m.where(Metric.timestamp >= since)
+ stmt_a = stmt_a.where(Anomaly.timestamp >= since)
+
+ stmt_m = stmt_m.order_by(Metric.timestamp.desc()).limit(max_points)
+ stmt_a = stmt_a.order_by(Anomaly.timestamp.desc()).limit(max_points)
+
+ metrics = list(reversed(db.scalars(stmt_m).all()))
+ anomalies = list(reversed(db.scalars(stmt_a).all()))
+
+ model_stmt = (
+ select(TrainedModel)
+ .where(TrainedModel.trained_by == "user")
+ .order_by(TrainedModel.trained_at.desc())
+ .limit(1)
+ )
+ model_record = db.scalars(model_stmt).first()
+
+ return DashboardOut(
+ devices=devices,
+ model=ModelInfo.model_validate(model_record) if model_record else None,
+ metrics=[DashboardMetric.model_validate(m) for m in metrics],
+ anomalies=[DashboardAnomaly.model_validate(a) for a in anomalies],
+ )
diff --git a/mona_core/routers/devices.py b/mona_core/routers/devices.py
new file mode 100644
index 0000000..88f245d
--- /dev/null
+++ b/mona_core/routers/devices.py
@@ -0,0 +1,94 @@
+import logging
+from typing import Sequence
+
+from fastapi import Depends, HTTPException, Query, status
+from sqlalchemy import delete, select
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.orm import Session
+
+from mona_core.db import Device
+from mona_core.schemas import (
+ DeviceOut,
+ MessageResponse,
+)
+from mona_core.security import (
+ admin_router,
+ get_db,
+ user_router,
+)
+from mona_core.validators import DeviceCreate
+
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
+
+
+@user_router.get("/devices", response_model=list[DeviceOut])
+def list_devices(
+ limit: int = Query(default=100, le=1000),
+ offset: int = Query(default=0, ge=0),
+ db: Session = Depends(get_db),
+) -> Sequence[Device]:
+ query = select(Device).order_by(Device.id).limit(limit).offset(offset)
+ return db.scalars(query).all()
+
+
+@admin_router.post(
+ "/devices", status_code=status.HTTP_201_CREATED, response_model=MessageResponse
+)
+def create_device(body: DeviceCreate, db: Session = Depends(get_db)) -> MessageResponse:
+ logger.debug("Attempting to create device", extra={"payload": body.model_dump()})
+
+ dev = Device(ip=body.ip, name=body.name, is_active=body.is_active)
+ db.add(dev)
+ try:
+ db.commit()
+ db.refresh(dev)
+ except IntegrityError:
+ db.rollback()
+ logger.warning(
+ "Device creating conflict error for device:",
+ extra={"ip": body.ip, "device": body.name, "is_active": body.is_active},
+ )
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="Name already exists",
+ )
+ except Exception:
+ db.rollback()
+ logger.exception("Error during creating device")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Server error"
+ )
+ logger.info(
+ "Device created",
+ extra={"ip": body.ip, "device": body.name, "is_active": body.is_active},
+ )
+ return MessageResponse(message="Device created")
+
+
+@admin_router.delete("/devices/{device_id}", response_model=MessageResponse)
+def delete_device(device_id: int, db: Session = Depends(get_db)) -> MessageResponse:
+ logger.debug("Attempting to delete device", extra={"device": device_id})
+ stmt = delete(Device).where(Device.id == device_id)
+ try:
+ result = db.execute(stmt)
+
+ if result.rowcount == 0: # type: ignore
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Device not found",
+ )
+
+ db.commit()
+
+ logger.info("Device deleted", extra={"device_id": device_id})
+ return MessageResponse(message="Device deleted")
+ except HTTPException:
+ raise
+ except Exception:
+ db.rollback()
+ logger.exception("Error during deleting device")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Server error",
+ )
diff --git a/mona_core/routers/health.py b/mona_core/routers/health.py
new file mode 100644
index 0000000..fb52a94
--- /dev/null
+++ b/mona_core/routers/health.py
@@ -0,0 +1,40 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from redis.exceptions import RedisError
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from mona_core.schemas import LivenessProbe, ReadinessProbe
+from mona_core.security import (
+ get_db,
+ redis_client,
+)
+
+router = APIRouter(tags=["Health"])
+
+
+@router.get("/health/live")
+def liveness_probe() -> LivenessProbe:
+ return LivenessProbe(status="alive")
+
+
+@router.get("/health/ready")
+async def readiness_probe(
+ db: Session = Depends(get_db),
+) -> ReadinessProbe:
+ try:
+ db.execute(text("SELECT 1"))
+ except Exception:
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="Database unavailable",
+ )
+
+ try:
+ await redis_client.ping()
+ except RedisError:
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="Redis unavailable",
+ )
+
+ return ReadinessProbe(status="ready")
diff --git a/mona_core/routers/model.py b/mona_core/routers/model.py
new file mode 100644
index 0000000..e88a452
--- /dev/null
+++ b/mona_core/routers/model.py
@@ -0,0 +1,124 @@
+import logging
+from datetime import UTC, datetime, timedelta
+
+from fastapi import Depends, HTTPException, Query, status
+from sqlalchemy import delete, select
+from sqlalchemy.orm import Session
+
+from mona_core.config import celery_client
+from mona_core.db import Anomaly, TrainedModel
+from mona_core.schemas import (
+ AnomalyOut,
+ AnomalySchema,
+ DeleteModelOut,
+ ModelInfo,
+ ModelInfoEmpty,
+ ModelInfoOk,
+ ModelInfoOut,
+ TrainModelOut,
+)
+from mona_core.security import (
+ admin_router,
+ get_db,
+ user_router,
+)
+
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
+
+
+@user_router.get("/anomalies", response_model=AnomalyOut)
+def get_anomalies(
+ hours: int = 24,
+ device: str | None = None,
+ limit: int = Query(default=100, le=1000),
+ offset: int = Query(default=0, ge=0),
+ db: Session = Depends(get_db),
+) -> AnomalyOut:
+ stmt = select(Anomaly)
+ if device:
+ stmt = stmt.where(Anomaly.device == device)
+ if hours > 0:
+ stmt = stmt.where(
+ Anomaly.timestamp >= datetime.now(UTC) - timedelta(hours=hours)
+ )
+
+ stmt = stmt.order_by(Anomaly.timestamp.desc()).limit(limit).offset(offset)
+ anomalies = db.execute(stmt).scalars().all()
+
+ return AnomalyOut(
+ items=[AnomalySchema.model_validate(a) for a in anomalies],
+ limit=limit,
+ offset=offset,
+ )
+
+
+@user_router.get("/model-info", response_model=ModelInfoOut)
+def model_info(db: Session = Depends(get_db)) -> ModelInfoOut:
+ stmt = (
+ select(TrainedModel)
+ .where(TrainedModel.trained_by == "user")
+ .order_by(TrainedModel.trained_at.desc())
+ .limit(1)
+ )
+
+ record = db.execute(stmt).scalar_one_or_none()
+
+ if record is None:
+ return ModelInfoEmpty(
+ status="no_model",
+ message="Model is not manually trained yet. Using auto-mode.",
+ )
+ model_data = ModelInfo.model_validate(record)
+ return ModelInfoOk(status="ok", model=model_data)
+
+
+@admin_router.post(
+ "/train", status_code=status.HTTP_202_ACCEPTED, response_model=TrainModelOut
+)
+def train_model(
+ hours: float = Query(
+ default=1.0,
+ ge=0,
+ le=24 * 7,
+ description="Hours of recent data to use for training",
+ ),
+ note: str = Query(default="", description="Comment (optional)"),
+) -> TrainModelOut:
+
+ task = celery_client.send_task(
+ "tasks.train_model_task", kwargs={"hours": hours, "note": note}
+ )
+
+ return TrainModelOut(
+ status="accepted",
+ message="Model training task has been submitted to the background.",
+ task_id=task.id,
+ )
+
+
+@admin_router.delete("/model", response_model=DeleteModelOut)
+def delete_model(db: Session = Depends(get_db)) -> DeleteModelOut:
+ """Deletes the custom model — Celery will return to auto-mode."""
+ try:
+ stmt = delete(TrainedModel).where(TrainedModel.trained_by == "user")
+ result = db.execute(stmt)
+ db.commit()
+ deleted = result.rowcount if result.rowcount else 0 # type: ignore
+ if deleted == 0:
+ return DeleteModelOut(
+ status="ok", deleted=deleted, message="No model to delete."
+ )
+ return DeleteModelOut(
+ status="ok",
+ deleted=deleted,
+ message="Model deleted. Celery switched to auto-mode.",
+ )
+
+ except Exception:
+ db.rollback()
+ logger.exception("Server error during creating task for deletion the model")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Server error",
+ )
diff --git a/mona_core/routers/monitoring.py b/mona_core/routers/monitoring.py
new file mode 100644
index 0000000..1bae6ca
--- /dev/null
+++ b/mona_core/routers/monitoring.py
@@ -0,0 +1,95 @@
+from datetime import UTC, datetime, timedelta
+
+from fastapi import APIRouter, Depends, Query
+from prometheus_client.core import REGISTRY, GaugeMetricFamily
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from mona_core.db import Anomaly, Device, Metric, SessionLocal
+from mona_core.schemas import MetricSchema, MetricsOut
+from mona_core.security import (
+ get_db,
+ user_router,
+)
+
+router_prometheus = APIRouter()
+
+
+class AnomalyCollector(object):
+ def collect(self):
+ gauge = GaugeMetricFamily(
+ "mona_anomaly_active",
+ "Active ML anomalies detected in the last 3 minutes",
+ labels=["device", "reason"],
+ )
+
+ db = SessionLocal()
+ try:
+ cutoff = datetime.now(UTC) - timedelta(minutes=0.2)
+
+ query = select(Anomaly).where(
+ Anomaly.timestamp >= cutoff.replace(tzinfo=None)
+ )
+ recent_anomalies = db.execute(query).scalars().all()
+
+ seen = set()
+ for a in recent_anomalies:
+ label_values = (a.device, a.reason)
+ if label_values not in seen:
+ gauge.add_metric([a.device, a.reason], 1)
+ seen.add(label_values)
+ except Exception as e:
+ print(f"Error in Prometheus AnomalyCollector: {e}")
+ finally:
+ db.close()
+
+ yield gauge
+
+
+try:
+ REGISTRY.register(AnomalyCollector())
+except ValueError:
+ pass
+
+
+@router_prometheus.get("/api/prometheus/targets", tags=["Prometheus"])
+def get_prometheus_targets(db: Session = Depends(get_db)):
+ stmt = select(Device).where(Device.is_active, Device.ip.is_not(None))
+ devices = db.execute(stmt).scalars().all()
+
+ targets = []
+ for dev in devices:
+ targets.append(
+ {
+ "targets": [f"{dev.ip}:9100"],
+ "labels": {
+ "job": dev.name,
+ "physical_pc": "true",
+ "device_label": dev.name,
+ },
+ }
+ )
+ return targets
+
+
+@user_router.get("/db-metrics", response_model=MetricsOut)
+def get_metrics(
+ device: str | None = None,
+ hours: int = Query(default=1, le=24 * 7),
+ before: datetime | None = None,
+ limit: int = Query(default=500, le=5000),
+ db: Session = Depends(get_db),
+) -> MetricsOut:
+ since = datetime.now(UTC) - timedelta(hours=hours)
+ stmt = select(Metric).where(Metric.timestamp >= since)
+ if device:
+ stmt = stmt.where(Metric.device == device)
+ if before:
+ stmt = stmt.where(Metric.timestamp < before)
+ stmt = stmt.order_by(Metric.timestamp.desc()).limit(limit)
+ rows = db.execute(stmt).scalars().all()
+
+ next_cursor = rows[-1].timestamp if len(rows) == limit else None
+ return MetricsOut(
+ items=[MetricSchema.model_validate(m) for m in rows], next_cursor=next_cursor
+ )
diff --git a/mona_core/routers/tasks.py b/mona_core/routers/tasks.py
new file mode 100644
index 0000000..c886bb0
--- /dev/null
+++ b/mona_core/routers/tasks.py
@@ -0,0 +1,19 @@
+from mona_core.config import celery_client
+from mona_core.schemas import TaskStatusOut
+from mona_core.security import user_router
+
+
+@user_router.get("/task-status/{task_id}", response_model=TaskStatusOut)
+def get_task_status(task_id: str) -> TaskStatusOut:
+ result = celery_client.AsyncResult(task_id)
+
+ raw = None
+ if result.ready():
+ try:
+ raw = result.result
+ if isinstance(raw, Exception):
+ raw = {"error": str(raw)}
+ except Exception:
+ raw = None
+
+ return TaskStatusOut(task_id=task_id, state=result.state, result=raw)
diff --git a/mona_core/routers/users.py b/mona_core/routers/users.py
new file mode 100644
index 0000000..d02d3ac
--- /dev/null
+++ b/mona_core/routers/users.py
@@ -0,0 +1,200 @@
+import logging
+
+from fastapi import Depends, HTTPException, Query, status
+from sqlalchemy import func, select
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.orm import Session
+
+from mona_core.db import Users
+from mona_core.schemas import (
+ ChangePassword,
+ ChangeRole,
+ CreateUser,
+ MessageResponse,
+ UserGetOut,
+ UserSession,
+)
+from mona_core.security import (
+ admin_router,
+ get_current_user,
+ get_db,
+ remove_all_sessions,
+)
+
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
+
+
+@admin_router.get("/users", response_model=list[UserGetOut])
+def get_users(
+ limit: int = Query(default=10, le=100),
+ offset: int = Query(default=0, ge=0),
+ db: Session = Depends(get_db),
+) -> list[UserGetOut]:
+ query = select(Users).order_by(Users.id).limit(limit).offset(offset)
+ res = db.execute(query).scalars().all()
+ return [UserGetOut.model_validate(u) for u in res]
+
+
+@admin_router.post("/user", response_model=MessageResponse)
+def create_user(
+ body: CreateUser,
+ db: Session = Depends(get_db),
+) -> MessageResponse:
+ logger.debug("Attempting to create a new user")
+ user = Users(username=body.username, role=body.role)
+ user.set_password(body.password)
+ db.add(user)
+ try:
+ db.commit()
+ db.refresh(user)
+ except IntegrityError:
+ db.rollback()
+ logger.warning("User creation conflict error")
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT,
+ detail="User already exists",
+ )
+ except Exception:
+ logger.exception("Server Error during user creation")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Server error"
+ )
+
+ logger.info("User created", extra={"username": body.role, "role": body.role})
+ return MessageResponse(message="User created")
+
+
+@admin_router.delete("/user/{user_id}", response_model=MessageResponse)
+async def delete_user(
+ user_id: int,
+ db: Session = Depends(get_db),
+) -> MessageResponse:
+ user_to_delete = db.execute(
+ select(Users).where(Users.id == user_id)
+ ).scalar_one_or_none()
+ if not user_to_delete:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found",
+ )
+
+ if user_to_delete.role == "admin":
+ admin_count = db.execute(
+ select(func.count()).select_from(Users).where(Users.role == "admin")
+ ).scalar_one()
+ if admin_count <= 1:
+ logger.warning("Attempting to delete the last remaining admin")
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Cannot delete the last remaining admin",
+ )
+
+ db.delete(user_to_delete)
+ try:
+ db.commit()
+
+ except Exception:
+ db.rollback()
+ logger.exception("Server error during user deletion")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Server error",
+ )
+
+ await remove_all_sessions(user_id)
+
+ logger.info("User deleted", extra={"user_id": user_id})
+ return MessageResponse(message="User deleted")
+
+
+@admin_router.patch("/user/change-password", response_model=MessageResponse)
+def change_user_password(
+ body: ChangePassword,
+ db: Session = Depends(get_db),
+) -> MessageResponse:
+ if not body.new_password.strip():
+ raise HTTPException(
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
+ detail="Password is required",
+ )
+
+ query = select(Users).where(Users.username == body.username)
+ res = db.execute(query).scalar_one_or_none()
+ if not res:
+ logger.warning("User not found for changing user password")
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Username invalid",
+ )
+ res.set_password(body.new_password)
+ try:
+ db.commit()
+ except Exception:
+ db.rollback()
+ logger.exception(
+ "Server error during password changing", extra={"username": body.username}
+ )
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Server error",
+ )
+ logger.info("Password changed for user", extra={"username": body.username})
+ return MessageResponse(message="Password changed")
+
+
+@admin_router.patch("/user/{user_id}/role", response_model=MessageResponse)
+def change_user_role(
+ user_id: int,
+ body: ChangeRole,
+ db: Session = Depends(get_db),
+ current_user: UserSession = Depends(get_current_user),
+) -> MessageResponse:
+ stmt = select(Users).where(Users.id == user_id)
+ user = db.execute(stmt).scalar_one_or_none()
+
+ if not user:
+ logger.warning("User not found for role changing")
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail={"status": "error", "message": "User not found"},
+ )
+
+ if user.username == current_user.username:
+ logger.warning(
+ "Attempting to change role to own role", extra={"username": user.username}
+ )
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="You cannot change your own role",
+ )
+ elif user.role == body.role:
+ logger.info("Role unchanged", extra={"username": user.username})
+ return MessageResponse(message="Role unchanged")
+ elif user.role == "admin" and body.role == "user":
+ admin_count = db.execute(
+ select(func.count()).select_from(Users).where(Users.role == "admin")
+ ).scalar_one()
+ if admin_count <= 1:
+ logger.warning(
+ "Attempting to demote the last remaining admin",
+ extra={"username": user.username},
+ )
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Cannot demote the last remaining admin",
+ )
+
+ user.role = body.role
+ try:
+ db.commit()
+ db.refresh(user)
+ except Exception:
+ db.rollback()
+ logger.exception("Server error during role changing")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Server error",
+ )
+ logger.info("Role switched", extra={"username": user.username})
+ return MessageResponse(message="Role switched")
diff --git a/mona_core/schemas.py b/mona_core/schemas.py
new file mode 100644
index 0000000..62cdecb
--- /dev/null
+++ b/mona_core/schemas.py
@@ -0,0 +1,177 @@
+from datetime import datetime
+from typing import Any, Literal, Optional, Union
+
+from pydantic import BaseModel, ConfigDict
+
+from mona_core.db import int_pk
+
+
+class LoginRequest(BaseModel):
+ username: str
+ password: str
+
+
+class CreateUser(BaseModel):
+ username: str
+ password: str
+ role: Literal["user", "admin"]
+
+
+class UserGetOut(BaseModel):
+ id: int_pk
+ username: str
+ role: Literal["user", "admin"]
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class DeviceOut(BaseModel):
+ id: int
+ ip: str
+ name: str
+ is_active: bool = True
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class AnomalySchema(BaseModel):
+ id: int_pk
+ metric_id: int
+ cpu: float
+ ram: float
+ timestamp: datetime
+ reason: str
+ score: float
+ detected_at: datetime
+ device: str
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class DashboardAnomaly(BaseModel):
+ id: int_pk
+ cpu: float
+ ram: float
+ timestamp: datetime
+ reason: str
+ score: float
+ device: str
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class DashboardMetric(BaseModel):
+ timestamp: datetime
+ cpu: float
+ ram: float
+ device: str
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class AnomalyOut(BaseModel):
+ items: list[AnomalySchema]
+ limit: int
+ offset: int
+
+
+class ModelInfo(BaseModel):
+ trained_at: datetime
+ trained_by: str
+ points_count: int
+ period_from: Optional[datetime] = None
+ period_to: Optional[datetime] = None
+ note: Optional[str] = None
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class ModelInfoOk(BaseModel):
+ status: Literal["ok"]
+ model: ModelInfo
+
+
+class ModelInfoEmpty(BaseModel):
+ status: Literal["no_model"]
+ message: str
+
+
+ModelInfoOut = Union[ModelInfoOk, ModelInfoEmpty]
+
+
+class TrainModelOut(BaseModel):
+ status: str
+ message: str
+ task_id: str
+
+
+class DeleteModelOut(BaseModel):
+ status: Literal["ok", "error"] = "ok"
+ deleted: int
+ message: str
+
+
+class ChangePassword(BaseModel):
+ username: str
+ new_password: str
+
+
+class ChangeRole(BaseModel):
+ role: Literal["user", "admin"]
+
+
+class MessageResponse(BaseModel):
+ status: Literal["ok", "error"] = "ok"
+ message: str
+
+
+class MetricSchema(BaseModel):
+ id: int_pk
+ cpu: float
+ ram: float
+ device: str
+ timestamp: datetime
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class MetricsOut(BaseModel):
+ items: list[MetricSchema]
+ next_cursor: Optional[datetime]
+
+
+class TaskStatusOut(BaseModel):
+ task_id: str
+ state: Optional[str]
+ result: Optional[Any]
+
+
+class DashboardOut(BaseModel):
+ devices: list[str]
+ model: Optional[ModelInfo]
+ metrics: list[DashboardMetric]
+ anomalies: list[DashboardAnomaly]
+
+
+class LoginOut(BaseModel):
+ status: Literal["ok"]
+ message: str
+ role: Literal["user", "admin"]
+
+
+class MeOut(BaseModel):
+ authenticated: bool
+
+
+class UserSession(BaseModel):
+ id: int
+ username: str
+ role: Literal["user", "admin"]
+
+
+class LivenessProbe(BaseModel):
+ status: Literal["alive"]
+
+
+class ReadinessProbe(BaseModel):
+ status: Literal["ready"]
diff --git a/mona_core/security.py b/mona_core/security.py
index b226484..31985f2 100644
--- a/mona_core/security.py
+++ b/mona_core/security.py
@@ -1,19 +1,24 @@
import json
+import logging
import os
import secrets
-from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
-from pydantic import BaseModel
-from redis.asyncio import from_url
+from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response, status
from sqlalchemy import select
from sqlalchemy.orm import Session
-from mona_core.config import settings
+from mona_core.config import redis_client, settings
from mona_core.db import SessionLocal, Users
+from mona_core.schemas import (
+ LoginOut,
+ LoginRequest,
+ MeOut,
+ MessageResponse,
+ UserSession,
+)
-# redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
-redis_url = settings.redis_url
-redis_client = from_url(redis_url, decode_responses=True)
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
# ─── helpers ────────────────────────────────────────────────────────────────
@@ -25,9 +30,29 @@ def get_db():
db.close()
-class LoginRequest(BaseModel):
- username: str
- password: str
+# ─── Sessions ───────────────────────────────────────────────────────────────
+
+
+def _user_sessions_key(user_id: int) -> str:
+ return f"user_sessions:{user_id}"
+
+
+async def register_sesion(user_id: int, session_id: str) -> None:
+ key = _user_sessions_key(user_id)
+ await redis_client.sadd(key, session_id)
+ await redis_client.expire(key, settings.session_ttl)
+
+
+async def unregister_session(user_id: int, session_id: str) -> None:
+ await redis_client.srem(_user_sessions_key(user_id), session_id)
+
+
+async def remove_all_sessions(user_id: int) -> None:
+ key = _user_sessions_key(user_id)
+ session_ids = await redis_client.smembers(key)
+ if session_ids:
+ await redis_client.delete(*[f"session:{sid}" for sid in session_ids]) # type: ignore[str-bytes-safe]
+ await redis_client.delete(key)
# ─── Startup ────────────────────────────────────────────────────────────────
@@ -47,8 +72,8 @@ def _seed_role(
usernames: list[str], passwords: list[str], role: str, db: Session
) -> None:
if len(usernames) != len(passwords):
- print(
- f"Warning: mismatched username/password count for role '{role}', skipping"
+ logger.warning(
+ f"mismatched username/password count for role '{role}', skipping"
)
return
for username, password in zip(usernames, passwords):
@@ -61,7 +86,7 @@ def _seed_role(
db.add(account)
-def seed_admin():
+def seed_admin() -> None:
admin_usernames, admin_passwords = _read_pair_list(
"ADMIN_USERNAMES", "ADMIN_PASSWORDS"
)
@@ -75,12 +100,12 @@ def seed_admin():
_seed_role(admin_usernames, admin_passwords, "admin", db)
_seed_role(user_usernames, user_passwords, "user", db)
db.commit()
- except Exception as e:
+ except Exception:
db.rollback()
- print(f"Error: {e}")
+ logger.exception("Error during seed_admin")
-# limiter
+# ─── limiter ────────────────────────────────────────────────────────────────
async def check_rate_limit(
key: str,
max_attempts: int = settings.max_attempts,
@@ -93,6 +118,7 @@ async def check_rate_limit(
if attempts > max_attempts:
ttl = await redis_client.ttl(attempts_key)
+ logger.warning("Rate limit exceeded", extra={"key": key, "attempts": attempts})
raise HTTPException(
status_code=429,
detail=f"Too many attempts. Try again in {ttl} seconds",
@@ -103,39 +129,48 @@ async def reset_rate_limit(key: str):
await redis_client.delete(f"login_attempts:{key}")
-# ─── Auth ────────────────────────────────────────────────────────────────
-async def get_current_user(user_session: str | None = Cookie(None)) -> dict:
+# ─── Auth ───────────────────────────────────────────────────────────────────
+async def get_current_user(user_session: str | None = Cookie(None)) -> UserSession:
if not user_session:
- raise HTTPException(status_code=401, detail="You are not auth(Cokie not found)")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="You are not auth(Cokie not found)",
+ )
raw = await redis_client.get(f"session:{user_session}")
if not raw:
- raise HTTPException(status_code=401, detail="Session expired or not valid")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Session expired or not valid",
+ )
- return json.loads(raw)
+ data = json.loads(raw)
+ return UserSession.model_validate(data)
-async def require_admin(user: dict = Depends(get_current_user)) -> dict:
- if user.get("role") != "admin":
- raise HTTPException(status_code=403, detail="Admin privileges required")
+async def require_admin(user: UserSession = Depends(get_current_user)) -> UserSession:
+ if user.role != "admin":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required"
+ )
return user
user_router = APIRouter(dependencies=[Depends(get_current_user)])
admin_router = APIRouter(dependencies=[Depends(require_admin)])
-# ─── Auth endpoints ───────────────────────────────────────────────────────────
+# ─── Auth endpoints ─────────────────────────────────────────────────────────
auth_router = APIRouter(tags=["Auth"])
# ─── login & logout ─────────────────────────────────────────────────────────
-@auth_router.post("/api/auth/login", tags=["Auth"])
+@auth_router.post("/auth/login", tags=["Auth"], response_model=LoginOut)
async def login(
body: LoginRequest,
request: Request,
response: Response,
db: Session = Depends(get_db),
-):
+) -> LoginOut:
ip = request.client.host if request.client else "127.0.0.1"
await check_rate_limit(f"ip:{ip}")
@@ -145,7 +180,10 @@ async def login(
user = db.execute(stmt).scalar_one_or_none()
if not user or not user.check_password(body.password):
- raise HTTPException(status_code=401, detail="Invalid username or password")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid username or password",
+ )
await reset_rate_limit(f"ip:{ip}")
await reset_rate_limit(f"user:{body.username}")
@@ -154,28 +192,37 @@ async def login(
session_data = json.dumps(
{"id": user.id, "username": user.username, "role": user.role}
)
- await redis_client.set(f"session:{session_id}", session_data, ex=43200)
+ await redis_client.set(
+ f"session:{session_id}", session_data, ex=settings.session_ttl
+ )
+ await register_sesion(user.id, session_id)
response.set_cookie(
key="user_session",
value=session_id,
httponly=True,
- secure=False,
+ secure=True,
samesite="lax",
max_age=43200,
)
- return {"status": "ok", "message": "Successfully login", "role": user.role}
+ return LoginOut(status="ok", message="Successfully login", role=user.role)
-@auth_router.post("/api/auth/logout", tags=["Auth"])
-async def logout(response: Response, user_session: str | None = Cookie(None)):
+@auth_router.post("/auth/logout", tags=["Auth"], response_model=MessageResponse)
+async def logout(
+ response: Response, user_session: str | None = Cookie(None)
+) -> MessageResponse:
if user_session:
+ raw = await redis_client.get(f"sesion:{user_session}")
+ if raw:
+ data = json.loads(raw)
+ await unregister_session(data["id"], user_session)
await redis_client.delete(f"session:{user_session}")
response.delete_cookie("user_session")
- return {"status": "ok", "message": "Successfuly logout"}
+ return MessageResponse(message="logged out")
-@auth_router.get("/api/auth/me")
-async def auth_me(admin=Depends(get_current_user)):
- return {"authenticated": True}
+@auth_router.get("/auth/me", response_model=MeOut)
+async def auth_me(admin=Depends(get_current_user)) -> MeOut:
+ return MeOut(authenticated=True)
diff --git a/mona_core/tasks.py b/mona_core/tasks.py
index 6d59298..a5e3019 100644
--- a/mona_core/tasks.py
+++ b/mona_core/tasks.py
@@ -1,15 +1,19 @@
+import logging
import os
from datetime import UTC, datetime, timedelta
-import requests
-
-# import pickle
+# import requests
+import httpx2
import skops.io as sio
from sqlalchemy import select
from mona_core.celery_conf import app
+from mona_core.config import settings
from mona_core.db import Device, Metric, SessionLocal, TrainedModel
+# ─── Logger ─────────────────────────────────────────────────────────────────
+logger = logging.getLogger(__name__)
+
# ─── Helpers ────────────────────────────────────────────────────────────────
PROMETHEUS_URL = os.getenv(
@@ -18,14 +22,34 @@
)
+# def _query(prometheus_url: str, query: str) -> float:
+# resp = requests.get(
+# f"{prometheus_url}/api/v1/query",
+# params={"query": query},
+# timeout=5,
+# )
+# data = resp.json()
+# return float(data["data"]["result"][0]["value"][1]) if data["data"]["result"] else 0
+
+
def _query(prometheus_url: str, query: str) -> float:
- resp = requests.get(
- f"{prometheus_url}/api/v1/query",
- params={"query": query},
- timeout=5,
- )
- data = resp.json()
- return float(data["data"]["result"][0]["value"][1]) if data["data"]["result"] else 0
+ # Использование httpx.Client или разового запроса httpx.get()
+ try:
+ resp = httpx2.get(
+ f"{prometheus_url}/api/v1/query",
+ params={"query": query},
+ timeout=5.0,
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ return (
+ float(data["data"]["result"][0]["value"][1])
+ if data["data"]["result"]
+ else 0.0
+ )
+ except (httpx2.HTTPError, KeyError, IndexError) as e:
+ logger.warning(f"Failed to query Prometheus: {e}")
+ return 0.0
def _build_features(rows):
@@ -46,6 +70,59 @@ def _build_features(rows):
# ─── Tasks ──────────────────────────────────────────────────────────────────
+# @app.task(name="tasks.collect_and_save")
+# def collect_and_save():
+# """CPU/RAM metrics from physical PCs via custom Prometheus."""
+# db = SessionLocal()
+# try:
+# config_devices = os.getenv("EXPORTERS", "").split(",")
+
+# for entry in config_devices:
+# if not entry or ":" not in entry:
+# continue
+
+# device_name, device_ip = entry.split(":", 1)
+# existing = db.execute(
+# select(Device).where(Device.name == device_name)
+# ).scalar_one_or_none()
+# if not existing:
+# new_dev = Device(name=device_name, ip=device_ip, is_active=True)
+# db.add(new_dev)
+# elif existing.ip != device_ip:
+# existing.ip = device_ip
+
+# db.commit()
+
+# active_devices = (
+# db.execute(select(Device).where(Device.is_active)).scalars().all()
+# )
+
+# results = []
+# for dev in active_devices:
+# job = dev.name
+# sel = f'job="{job}",physical_pc="true"'
+# cpu_query = f'100 * (1 - avg(rate(node_cpu_seconds_total{{{sel},mode="idle"}}[5m])))'
+# ram_query = (
+# f"avg((1 - (node_memory_MemAvailable_bytes{{{sel}}}"
+# f" / node_memory_MemTotal_bytes{{{sel}}})) * 100)"
+# )
+# cpu = _query(PROMETHEUS_URL, cpu_query)
+# ram = _query(PROMETHEUS_URL, ram_query)
+
+# metric = Metric(cpu=cpu, ram=ram, device=job)
+# db.add(metric)
+# results.append({"device": job, "cpu": round(cpu, 2), "ram": round(ram, 2)})
+
+# db.commit()
+# return results
+# except Exception as e:
+# db.rollback()
+# logger.exception("Error collecting metrics")
+# return {"error": str(e)}
+# finally:
+# db.close()
+
+
@app.task(name="tasks.collect_and_save")
def collect_and_save():
"""CPU/RAM metrics from physical PCs via custom Prometheus."""
@@ -74,26 +151,50 @@ def collect_and_save():
)
results = []
- for dev in active_devices:
- job = dev.name
- sel = f'job="{job}",physical_pc="true"'
- cpu_query = f'100 * (1 - avg(rate(node_cpu_seconds_total{{{sel},mode="idle"}}[5m])))'
- ram_query = (
- f"avg((1 - (node_memory_MemAvailable_bytes{{{sel}}}"
- f" / node_memory_MemTotal_bytes{{{sel}}})) * 100)"
- )
- cpu = _query(PROMETHEUS_URL, cpu_query)
- ram = _query(PROMETHEUS_URL, ram_query)
-
- metric = Metric(cpu=cpu, ram=ram, device=job)
- db.add(metric)
- results.append({"device": job, "cpu": round(cpu, 2), "ram": round(ram, 2)})
-
- db.commit()
- return results
+ with httpx2.Client(base_url=PROMETHEUS_URL, timeout=0.5) as client:
+ for dev in active_devices:
+ job = dev.name
+ sel = f'job="{job}",physical_pc="true"'
+ cpu_query = f'100 * (1 - avg(rate(node_cpu_seconds_total{{{sel},mode="idle"}}[5m])))'
+ ram_query = (
+ f"avg((1 - (node_memory_MemAvailable_bytes{{{sel}}}"
+ f" / node_memory_MemTotal_bytes{{{sel}}})) * 100)"
+ )
+ try:
+ cpu_resp = client.get(
+ "/api/v1/query", params={"query": cpu_query}
+ ).json()
+ ram_resp = client.get(
+ "/api/v1/query", params={"query": ram_query}
+ ).json()
+
+ cpu = (
+ float(cpu_resp["data"]["result"][0]["value"][1])
+ if cpu_resp.get("data", {}).get("result")
+ else 0.0
+ )
+ ram = (
+ float(ram_resp["data"]["result"][0]["value"][1])
+ if ram_resp.get("data", {}).get("result")
+ else 0.0
+ )
+ except (httpx2.HTTPError, KeyError, IndexError) as err:
+ logger.warning(
+ f"Failed to fetch Prometheus metrics for {job}: {err}"
+ )
+ cpu, ram = 0.0, 0.0
+
+ metric = Metric(cpu=cpu, ram=ram, device=job)
+ db.add(metric)
+ results.append(
+ {"device": job, "cpu": round(cpu, 2), "ram": round(ram, 2)}
+ )
+
+ db.commit()
+ return results
except Exception as e:
db.rollback()
- print(f"Error collecting metrics: {e}")
+ logger.exception("Error collecting metrics")
return {"error": str(e)}
finally:
db.close()
@@ -130,7 +231,7 @@ def train_model_task(hours: float, note: str):
model = IsolationForest(
n_estimators=200,
- contamination=0.03,
+ contamination=settings.contamination,
random_state=42,
)
model.fit(X_scaled)
@@ -153,8 +254,9 @@ def train_model_task(hours: float, note: str):
"message": f"Model trained on {len(rows)} points over the last {hours} h.",
}
- except Exception as e:
+ except Exception:
db.rollback()
- return {"status": "error", "message": str(e)}
+ logger.exception("Server error during training model")
+ return {"status": "error", "message": "Server error"}
finally:
db.close()
diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml
new file mode 100644
index 0000000..aaf519f
--- /dev/null
+++ b/prometheus/prometheus.yml
@@ -0,0 +1,23 @@
+global:
+ scrape_interval: 15s
+
+scrape_configs:
+ # Static list of physical PCs, equivalent to `externalPcExporters` in
+ # values.yaml. Prefer managing devices via the Admin panel instead —
+ # this is only useful for bulk-adding devices at startup.
+ - job_name: 'static-exporters'
+ static_configs:
+ # - targets: ['10.0.0.8:9100']
+ # labels:
+ # job: 'pc-test'
+ # physical_pc: "true"
+
+ # Dynamic list, pulled live from the API — devices added/removed via
+ # the Admin panel or the API show up here automatically.
+ - job_name: 'dynamic-exporters'
+ http_sd_configs:
+ - url: 'http://api:8000/api/prometheus/targets'
+ refresh_interval: 30s
+ relabel_configs:
+ - target_label: physical_pc
+ replacement: "true"
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 7b513c2..2946af3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,20 +5,14 @@ description = "toml for mona project"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
- "alembic>=1.18.5",
"bcrypt>=5.0.0",
"celery>=5.6.3",
- "dotenv>=0.9.9",
- "fastapi>=0.139.0",
- "prometheus-fastapi-instrumentator>=8.0.2",
+ "httpx2>=2.12.0",
+ "pydantic>=2.13.5",
+ "pydantic-settings>=2.15.0",
+ "redis>=8.1.0",
+ "sqlalchemy>=2.0.52",
"psycopg2-binary>=2.9.12",
- "pydantic-settings>=2.14.2",
- "pyyaml>=6.0.1",
- "redis>=8.0.1",
- "requests>=2.34.2",
- "skops>=0.14.0",
- "sqlalchemy>=2.0.51",
- "uvicorn>=0.51.0",
]
[tool.ruff]
@@ -32,6 +26,9 @@ ignore = ["E501"]
[tool.ruff.lint.pep8-naming]
ignore-names = ["X"]
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = ["S101"]
+
[tool.mypy]
ignore_missing_imports = true
disable_error_code = "attr-defined"
@@ -44,34 +41,45 @@ pythonpath = ["."]
[dependency-groups]
ci = [
- "celery>=5.6.3",
+ "fakeredis>=2.37.1",
"fastapi>=0.139.0",
"httpx>=0.28.1",
- "httpx2>=2.5.0",
"mypy>=2.2.0",
"prometheus-fastapi-instrumentator>=8.0.2",
"pydantic>=2.13.4",
"pytest>=9.1.1",
+ "pytest-asyncio>=1.4.0",
"ruff>=0.15.21",
- "sqlalchemy>=2.0.51",
]
dev = [
"celery>=5.6.3",
"fastapi>=0.139.0",
"httpx>=0.28.1",
- "httpx2>=2.5.0",
"numpy>=2.5.1",
"prometheus-fastapi-instrumentator>=8.0.2",
- "psycopg2-binary>=2.9.12",
"pydantic>=2.13.4",
"pytest>=9.1.1",
"pyyaml>=6.0.1",
- "redis>=8.0.1",
- "requests>=2.34.2",
"ruff>=0.15.21",
"scikit-learn>=1.9.0",
- "sqlalchemy>=2.0.51",
"uvicorn>=0.51.0",
- "scikit-learn>=1.9.0",
"checkov>=3.3.8",
+ "pre-commit>=4.6.1",
+ "fakeredis>=2.37.1",
+ "pytest-asyncio>=1.4.0",
+ "mypy>=2.3.1",
+ "skops>=0.14.0",
+ "alembic>=1.19.1",
+]
+docker-fastapi = [
+ "alembic>=1.19.1",
+ "fastapi>=0.141.1",
+ "prometheus-fastapi-instrumentator>=8.1.0",
+ "uvicorn>=0.52.4",
+]
+worker = [
+ "numpy>=2.5.1",
+ "pyyaml>=6.0.1",
+ "scikit-learn>=1.9.0",
+ "skops>=0.14.0",
]
diff --git a/terraform/main.tf b/terraform/main.tf
index adb47dc..fd07fc8 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -20,17 +20,6 @@ terraform {
}
}
-# ─── locals ───────────────────────────────────────────────────────────────
-# locals {
-# postgres_env = {
-# "POSTGRES_USER" = var.postgres_user
-# "POSTGRES_PASSWORD" = random_password.postgres_password.result
-# "POSTGRES_DB" = var.postgres_db
-# "POSTGRES_HOST" = "postgres"
-# "POSTGRES_PORT" = "5432"
-# }
-# }
-
# ─── providers ───────────────────────────────────────────────────────────────
provider "kind" {}
@@ -143,26 +132,22 @@ resource "helm_release" "mona_app" {
values = [
file("${path.module}/../mona-chart/values.yaml"),
- # file("${path.module}/../mona-chart/values-prod.yaml"),
]
- # dynamic "set_sensitive" {
- # for_each = local.postgres_env
- # iterator = env
- # content {
- # name = "fastapi.env.${env.key}"
- # value = env.value
- # }
- # }
-
- # dynamic "set_sensitive" {
- # for_each = local.postgres_env
- # iterator = env
- # content {
- # name = "celeryWorker.env.${env.key}"
- # value = env.value
- # }
- # }
+ set {
+ name = "fastapi.env.LOG_LEVEL_FASTAPI"
+ value = var.LOG_LEVEL_FASTAPI
+ }
+
+ set {
+ name = "celeryBeat.env.LOG_LEVEL_CELERY_BEAT"
+ value = var.LOG_LEVEL_CELERY_BEAT
+ }
+
+ set {
+ name = "celeryWorker.env.LOG_LEVEL_CELERY_WORKER"
+ value = var.LOG_LEVEL_CELERY_WORKER
+ }
set {
name = "postgres.auth.user"
@@ -211,7 +196,6 @@ resource "helm_release" "mona_app" {
value = var.grafana_admin_password
}
- # Ждем и Loki, И успешную загрузку Docker-образов в Kind!
depends_on = [
helm_release.loki_stack,
null_resource.kind_load_images
@@ -227,4 +211,4 @@ output "postgres_password" {
output "kubeconfig_path" {
value = kind_cluster.mona_cluster.kubeconfig_path
-}
\ No newline at end of file
+}
diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example
index fe8c0e9..2e50baa 100644
--- a/terraform/terraform.tfvars.example
+++ b/terraform/terraform.tfvars.example
@@ -3,15 +3,21 @@
# Telegram alerts
telegram_bot_token = ""
telegram_chat_id = ""
-# Username and passowrd for admins and users
+# Username and password for admins and users
admin_username = ["admin", "admin2"]
admin_password = ["admin123", "test_123"]
user_username = ["user1"]
user_password = ["userpass1"]
+# Password for postgres generates automatically during terraform deploy via terraform
postgres_user = "myuser"
postgres_db = "mydb"
postgres_port = 5432
-# Passowrd for grafana
-grafana_admin_password = "admin123"
\ No newline at end of file
+# Password for grafana
+grafana_admin_password = "admin123"
+
+# Log level like ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), "INFO" by default
+LOG_LEVEL_FASTAPI = ""
+LOG_LEVEL_CELERY_BEAT = ""
+LOG_LEVEL_CELERY_WORKER = ""
\ No newline at end of file
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 5c8bf26..3b6128d 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -42,4 +42,16 @@ variable "postgres_port" {
variable "grafana_admin_password" {
type = string
sensitive = true
+}
+
+variable "LOG_LEVEL_FASTAPI" {
+ type = string
+}
+
+variable "LOG_LEVEL_CELERY_BEAT" {
+ type = string
+}
+
+variable "LOG_LEVEL_CELERY_WORKER" {
+ type = string
}
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 69f370b..dbaf3e6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@
import pytest
from fastapi.testclient import TestClient
+from fakeredis import FakeAsyncRedis
_tmp_dir = tempfile.mkdtemp()
_TEST_DB_PATH = os.path.join(_tmp_dir, "test.db")
@@ -13,6 +14,9 @@
from mona_core import db as db_module # noqa: E402
from mona_core import main as main_module # noqa: E402
from mona_core import security as security_module # noqa: E402
+from mona_core import tasks as tasks_module # noqa: E402
+from mona_core.routers import health as health_module # noqa: E402
+from mona_core.config import celery_client # noqa: E402
@pytest.fixture(scope="session", autouse=True)
@@ -74,76 +78,127 @@ def fake_async_result(task_id):
state["async_result_calls"].append(task_id)
return state["async_result_return"]
- monkeypatch.setattr(main_module.celery_client, "send_task", fake_send_task)
- monkeypatch.setattr(main_module.celery_client, "AsyncResult", fake_async_result)
+ monkeypatch.setattr(celery_client, "send_task", fake_send_task)
+ monkeypatch.setattr(celery_client, "AsyncResult", fake_async_result)
return state
@pytest.fixture()
def mock_user_auth():
+ from mona_core.schemas import UserSession
from mona_core.security import get_current_user
-
- main_module.app.dependency_overrides[get_current_user] = lambda: {
- "id": 1,
- "username": "mock_user_123",
- "role": "user",
- }
- yield
+ user_session = UserSession(
+ id=1,
+ username="mock_user_123",
+ role="user",
+ )
+ main_module.app.dependency_overrides[get_current_user] = lambda: user_session
+ yield user_session
main_module.app.dependency_overrides.clear()
@pytest.fixture()
def mock_admin_auth():
+ from mona_core.schemas import UserSession
from mona_core.security import get_current_user
-
- main_module.app.dependency_overrides[get_current_user] = lambda: {
- "id": 2,
- "username": "mock_admin_123",
- "role": "admin",
- }
- yield
+ admin_session = UserSession(
+ id=2,
+ username="mock_admin_123",
+ role="admin",
+ )
+ main_module.app.dependency_overrides[get_current_user] = lambda: admin_session
+ yield admin_session
main_module.app.dependency_overrides.clear()
+@pytest.fixture()
+def make_user(db_session):
+ """Factory fixture: creates a Users row directly in the DB (bypassing the API).
+
+ Usage:
+ def test_x(self, make_user):
+ admin = make_user(username="alice", password="secret123", role="admin")
+ """
+ from mona_core.db import Users
+
+ def _make(username="testuser", password="testpass123", role="user"):
+ user = Users(username=username, role=role)
+ user.set_password(password)
+ db_session.add(user)
+ db_session.commit()
+ db_session.refresh(user)
+ return user
+
+ return _make
+
+@pytest.fixture()
+def mock_redis(monkeypatch):
+ fake = FakeAsyncRedis(decode_responses=True)
+ monkeypatch.setattr(security_module, "redis_client", fake)
+ monkeypatch.setattr(health_module, "redis_client", fake)
+ return fake
-class FakeRedis:
- def __init__(self):
- self.storage = {}
+# ─── Prometheus mocks (used by tasks._query / tasks.collect_and_save) ───────
+class FakeResponse:
+ """Mimics an httpx2 Response, for mocking tasks.httpx2.get() calls."""
- async def set(self, key, value, ex=None):
- self.storage[key] = value
- return True
+ def __init__(self, value=None, empty=False):
+ self._value = value
+ self._empty = empty
- async def get(self, key):
- return self.storage.get(key)
+ def raise_for_status(self):
+ pass
- async def delete(self, key):
- self.storage.pop(key, None)
- return 1
+ def json(self):
+ if self._empty:
+ return {"data": {"result": []}}
+ return {"data": {"result": [{"value": [123, str(self._value)]}]}}
- async def incr(self, key):
- val = self.storage.get(key, 0)
-
- if isinstance(val, (str, bytes)):
- val = int(val)
-
- new_val = val + 1
- self.storage[key] = new_val
-
- return new_val
+class FakeClient:
+ """Mimics httpx2.Client(base_url=..., timeout=...) used as a context manager.
+ `values` is an iterable of numbers consumed in call order: collect_and_save()
+ issues one GET for the cpu query, then one for the ram query, per device.
+ So values = [cpu1, ram1, cpu2, ram2, ...].
+ """
- async def expire(self, key, time):
- if key in self.storage:
- return True
+ def __init__(self, values):
+ self._values = iter(values)
+ self.calls = []
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc_info):
return False
- async def ttl(self, key):
- if key in self.storage:
- return 300
- return -2
-
+ def get(self, path, params=None):
+ self.calls.append(params)
+ return FakeResponse(value=next(self._values))
+
+
@pytest.fixture()
-def mock_redis(monkeypatch):
- fake = FakeRedis()
- monkeypatch.setattr(security_module, "redis_client", fake)
- return fake
+def fake_prometheus_response():
+ """Factory for FakeResponse objects, to mock tasks.httpx2.get() directly.
+ Usage:
+ monkeypatch.setattr(tasks.httpx2, "get", lambda *a, **k: fake_prometheus_response(57.8))
+ """
+
+ def _make(value=None, empty=False):
+ return FakeResponse(value=value, empty=empty)
+ return _make
+
+
+@pytest.fixture()
+def patch_prometheus_client(monkeypatch):
+ """Patches tasks.httpx2.Client with a FakeClient that yields `values` in order.
+ Usage:
+ def test_x(self, monkeypatch, db_session, patch_prometheus_client):
+ patch_prometheus_client([55.5, 77.7]) # cpu, ram for one device
+ tasks.collect_and_save()
+ """
+
+ def _patch(values):
+ fake_client = FakeClient(values)
+ monkeypatch.setattr(tasks_module.httpx2, "Client", lambda *a, **k: fake_client)
+ return fake_client
+ return _patch
diff --git a/tests/test_anomalies.py b/tests/test_anomalies.py
new file mode 100644
index 0000000..ec3bf52
--- /dev/null
+++ b/tests/test_anomalies.py
@@ -0,0 +1,73 @@
+from datetime import UTC, datetime, timedelta
+
+from mona_core.db import Anomaly
+
+class TestAnomalies:
+ def _make_anomaly(self, device, timestamp, score=0.9):
+ return Anomaly(
+ metric_id=1,
+ cpu=90.0,
+ ram=80.0,
+ timestamp=timestamp,
+ reason="cpu_spike",
+ score=score,
+ device=device,
+ )
+
+ def test_get_anomalies_default_window(self, client, db_session, mock_user_auth):
+ now = datetime.now(UTC)
+ db_session.add_all(
+ [
+ self._make_anomaly("srv-1", now - timedelta(hours=1)),
+ self._make_anomaly("srv-1", now - timedelta(hours=48)),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/anomalies")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert len(body) == 3
+ assert body["items"][0]["device"] == "srv-1"
+
+ def test_get_anomalies_filter_by_device(self, client, db_session, mock_user_auth):
+ now = datetime.now(UTC)
+ db_session.add_all(
+ [
+ self._make_anomaly("srv-1", now),
+ self._make_anomaly("srv-2", now),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/anomalies", params={"device": "srv-2"})
+ assert resp.status_code == 200
+ body = resp.json()
+ assert len(body) == 3
+ assert body["items"][0]["device"] == "srv-2"
+
+ def test_get_anomalies_hours_zero_disables_time_filter(
+ self, client, db_session, mock_user_auth
+ ):
+ now = datetime.now(UTC)
+ db_session.add(self._make_anomaly("srv-1", now - timedelta(days=30)))
+ db_session.commit()
+
+ resp = client.get("/api/v1/anomalies", params={"hours": 0})
+ assert resp.status_code == 200
+ assert len(resp.json()) == 3
+
+ def test_get_anomalies_ordered_desc(self, client, db_session, mock_user_auth):
+ now = datetime.now(UTC)
+ db_session.add_all(
+ [
+ self._make_anomaly("srv-1", now - timedelta(minutes=10)),
+ self._make_anomaly("srv-1", now - timedelta(minutes=1)),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/anomalies")
+ body = resp.json()
+ timestamps = [a["timestamp"] for a in body["items"]]
+ assert timestamps == sorted(timestamps, reverse=True)
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index 8f09630..0000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,674 +0,0 @@
-from datetime import UTC, datetime, timedelta
-
-import pytest
-
-from mona_core.db import Anomaly, Device, Metric, TrainedModel, Users
-
-
-class TestProbes:
- def test_liveness_probe(self, client):
- resp = client.get("/health/live")
- assert resp.status_code == 200
- assert resp.json() == {"status": "alive"}
-
- def test_readiness_probe_ok(self, client):
- resp = client.get("/health/ready")
- assert resp.status_code == 200
- assert resp.json() == {"status": "ready"}
-
- def test_readiness_probe_db_down(self, client, monkeypatch):
- from sqlalchemy.orm import Session
-
- def broken_execute(self, *args, **kwargs):
- raise Exception("connection refused")
-
- monkeypatch.setattr(Session, "execute", broken_execute)
-
- resp = client.get("/health/ready")
- assert resp.status_code == 503
- assert resp.json()["detail"] == "Database unavailable"
-
-
-class TestDevices:
- def test_get_device_empty(self, client, mock_user_auth):
- resp = client.get("/devices")
- assert resp.status_code == 200
- assert resp.json() == []
-
- def test_create_device(self, client, mock_admin_auth):
- payload = {"ip": "192.168.1.10", "name": "office-pc-1", "is_active": True}
- resp = client.post("/devices", json=payload)
-
- assert resp.status_code == 201
- body = resp.json()
- assert body["ip"] == payload["ip"]
- assert body["name"] == payload["name"]
- assert body["is_active"] is True
- assert "id" in body
-
- def test_create_device_default_active(self, client, mock_admin_auth):
- payload = {"ip": "192.168.1.10", "name": "office-pc-1"}
- resp = client.post("/devices", json=payload)
- assert resp.status_code == 201
- assert resp.json()["is_active"] is True
-
- def test_create_device_duplicate_name_conflict(self, client, mock_admin_auth):
- payload = {"ip": "10.0.0.1", "name": "dup-name"}
- first = client.post("/devices", json=payload)
- assert first.status_code == 201
-
- second = client.post("/devices", json={"ip": "10.0.0.2", "name": "dup-name"})
- assert second.status_code == 409
- assert second.json()["detail"] == "Name already exists"
-
- def test_list_devices_after_create(self, client, mock_admin_auth):
- client.post("/devices", json={"ip": "10.0.0.1", "name": "a"})
- client.post("/devices", json={"ip": "10.0.0.2", "name": "b"})
-
- resp = client.get("/devices")
- assert resp.status_code == 200
- names = {d["name"] for d in resp.json()}
- assert names == {"a", "b"}
-
- def test_delete_device_success(self, client, db_session, mock_admin_auth):
- dev = Device(ip="10.0.0.9", name="to-delete")
- db_session.add(dev)
- db_session.commit()
- db_session.refresh(dev)
-
- resp = client.delete(f"/devices/{dev.id}")
- assert resp.status_code == 200
- assert resp.json() == {"message": "Device deleted successfully"}
-
- resp2 = client.get("/devices")
- assert resp2.json() == []
-
- def test_delete_device_not_found(self, client, mock_admin_auth):
- resp = client.delete("/devices/99999")
- assert resp.status_code == 404
- assert resp.json()["detail"] == "Device not found"
-
-
-class TestValidation:
- @pytest.mark.parametrize(
- "ip, name",
- [
- ("10.0.0", "test"),
- ("10", "test-10"),
- ("test", "srv-10"),
- ("2001:db8::1::2", "pc-12"),
- ("gggg::1", "srv-ipv6"),
- ("12345::1", "test-ipv6"),
- ],
- )
- def test_create_device_invalid_ip(self, client, ip, name, mock_admin_auth):
- payload = {"ip": ip, "name": name, "is_active": True}
- resp = client.post("/devices", json=payload)
-
- assert resp.status_code == 422
- assert (
- resp.json()["detail"][0]["msg"]
- == "Value error, Must be a valid IPv4 or IPv6 address"
- )
-
- @pytest.mark.parametrize(
- "ip, name",
- [
- ("172.16.0.5", "{1==1}"),
- ("10.0.1.10", "bad_u$er}"),
- ("192.168.0.10", "} job {"),
- ("::1", "pc 12"),
- ("2001:db8::ff00:42:8329", "1234567890101112"),
- ("2001:0db8:0000:0000:0000:8a2e:0370:7334", "SRV-123456789101111"),
- ],
- )
- def test_create_device_invalid_name(self, ip, name, client, mock_admin_auth):
- payload = {"ip": ip, "name": name, "is_active": True}
- resp = client.post("/devices", json=payload)
-
- assert resp.status_code == 422
- assert (
- resp.json()["detail"][0]["msg"]
- == "Value error, Name can only contain letters, numbers, '_' and '-' (up to 15 characters)"
- )
-
- @pytest.mark.parametrize(
- "ip, name",
- [
- ("192.168.1.20", "a" * 15),
- ("192.168.1.21", "a"),
- ("192.168.1.22", "srv_01-test"),
- ("2001:db8::1", "ipv6-device"),
- ],
- )
- def test_create_device_valid_name(self, client, ip, name, mock_admin_auth):
- resp = client.post("/devices", json={"ip": ip, "name": name, "is_active": True})
- assert resp.status_code == 201
- assert resp.json()["name"] == name
-
- def test_create_device_ip_leading_zeros_rejected(self, client, mock_admin_auth):
- resp = client.post(
- "/devices",
- json={"ip": "192.168.001.001", "name": "test", "is_active": True},
- )
- assert resp.status_code == 422
-
- def test_create_device_ip_cidr_rejected(self, client, mock_admin_auth):
- resp = client.post(
- "/devices", json={"ip": "10.0.0.1/24", "name": "test", "is_active": True}
- )
- assert resp.status_code == 422
-
- def test_create_device_trims_whitespace(self, client, mock_admin_auth):
- resp = client.post(
- "/devices",
- json={
- "ip": " 192.168.1.30 ",
- "name": " trimmed-name ",
- "is_active": True,
- },
- )
- assert resp.status_code == 201
- body = resp.json()
- assert body["ip"] == "192.168.1.30"
- assert body["name"] == "trimmed-name"
-
- def test_create_device_missing_name(self, client, mock_admin_auth):
- resp = client.post("/devices", json={"ip": "192.168.1.40", "is_active": True})
- assert resp.status_code == 422
- assert resp.json()["detail"][0]["type"] == "missing"
-
- def test_create_device_name_wrong_type(self, client, mock_admin_auth):
- resp = client.post(
- "/devices", json={"ip": "192.168.1.41", "name": 12345, "is_active": True}
- )
- assert resp.status_code == 422
-
- def test_create_device_ipv4_mapped_ipv6(self, client, mock_admin_auth):
- resp = client.post(
- "/devices",
- json={"ip": "::ffff:192.168.1.1", "name": "mapped-ipv6", "is_active": True},
- )
- assert resp.status_code == 201
- assert resp.json()["ip"] == "::ffff:192.168.1.1"
-
-
-class TestModel:
- def test_model_info_no_model(self, client, mock_user_auth):
- resp = client.get("/model-info")
- assert resp.status_code == 200
- assert resp.json() == {
- "status": "no_model",
- "message": "Model is not manually trained yet. Using auto-mode.",
- }
-
- def test_model_info_with_model(self, client, db_session, mock_user_auth):
- db_session.add(
- TrainedModel(
- model_data=b"binary-blob",
- trained_by="user",
- points_count=1000,
- period_from=datetime(2026, 1, 1, tzinfo=UTC),
- period_to=datetime(2026, 1, 2, tzinfo=UTC),
- note="nightly training",
- )
- )
- db_session.commit()
-
- resp = client.get("/model-info")
- assert resp.status_code == 200
- body = resp.json()
- assert body["status"] == "ok"
- assert body["model"]["points_count"] == 1000
- assert body["model"]["note"] == "nightly training"
-
- def test_model_info_ignores_non_user_trained(
- self, client, db_session, mock_user_auth
- ):
- db_session.add(
- TrainedModel(
- model_data=b"x",
- trained_by="auto",
- points_count=1,
- period_from=datetime(2026, 1, 1, tzinfo=UTC),
- period_to=datetime(2026, 1, 2, tzinfo=UTC),
- note="",
- )
- )
- db_session.commit()
-
- resp = client.get("/model-info")
- assert resp.json()["status"] == "no_model"
-
- def test_delete_model(self, client, db_session, mock_admin_auth):
- db_session.add(
- TrainedModel(
- model_data=b"x",
- trained_by="user",
- points_count=1,
- period_from=datetime(2026, 1, 1, tzinfo=UTC),
- period_to=datetime(2026, 1, 2, tzinfo=UTC),
- note="",
- )
- )
- db_session.commit()
-
- resp = client.delete("/model")
- assert resp.status_code == 200
- body = resp.json()
- assert body["status"] == "ok"
- assert body["deleted"] == 1
-
- assert client.get("/model-info").json()["status"] == "no_model"
-
- def test_delete_model_when_none_exists(self, client, mock_admin_auth):
- resp = client.delete("/model")
- assert resp.status_code == 200
- assert resp.json()["deleted"] == 0
-
-
-class TestDashboardAndMetrics:
- def test_db_metrics_empty(self, client, mock_user_auth):
- resp = client.get("/db-metrics")
- assert resp.status_code == 200
- assert resp.json() == {"items": [], "next_cursor": None}
-
- def test_db_metrics_filter_by_device(self, client, db_session, mock_user_auth):
- db_session.add_all(
- [
- Metric(cpu=10, ram=20, device="srv-1"),
- Metric(cpu=30, ram=40, device="srv-2"),
- ]
- )
- db_session.commit()
-
- resp = client.get("/db-metrics", params={"device": "srv-1"})
- body = resp.json()
- assert len(body) == 2
- assert body["items"][0]["device"] == "srv-1"
-
- def test_dashboard_combines_metrics_anomalies_and_model(
- self, client, db_session, mock_user_auth
- ):
- now = datetime.now(UTC)
- db_session.add_all(
- [
- Metric(cpu=50, ram=60, device="srv-1", timestamp=now),
- Anomaly(
- metric_id=1,
- cpu=95,
- ram=90,
- timestamp=now,
- reason="cpu_spike",
- score=0.99,
- device="srv-1",
- ),
- ]
- )
- db_session.commit()
-
- resp = client.get("/api/dashboard", params={"hours": 1})
- assert resp.status_code == 200
- body = resp.json()
-
- assert "srv-1" in body["devices"]
- assert len(body["metrics"]) == 1
- assert len(body["anomalies"]) == 1
- assert body["model"] is None
-
- def test_dashboard_respects_time_window(self, client, db_session, mock_user_auth):
- old = datetime.now(UTC) - timedelta(hours=5)
- db_session.add(Metric(cpu=1, ram=1, device="srv-1", timestamp=old))
- db_session.commit()
-
- resp = client.get("/api/dashboard", params={"hours": 1})
- assert resp.json()["metrics"] == []
-
-
-class TestTasks:
- def test_train_model_submits_task(self, client, mock_celery, mock_admin_auth):
- resp = client.post("/train", params={"hours": 2.5, "note": "manual run"})
-
- assert resp.status_code == 202
- body = resp.json()
- assert body["status"] == "accepted"
- # assert body["task_id"] == "fake-task-id-123"
- assert "task_id" in body
-
- assert len(mock_celery["send_task_calls"]) == 1
- call = mock_celery["send_task_calls"][0]
- assert call["name"] == "tasks.train_model_task"
- assert call["kwargs"] == {"hours": 2.5, "note": "manual run"}
-
- def test_train_model_defaults(self, client, mock_celery, mock_admin_auth):
- resp = client.post("/train")
- assert resp.status_code == 202
- # call = mock_celery["send_task_calls"][0]
- # assert call["kwargs"] == {"hours": 1.0, "note": ""}
- assert len(mock_celery["send_task_calls"]) == 1
- call = mock_celery["send_task_calls"][0]
- assert call["name"] == "tasks.train_model_task"
-
- def test_task_status_pending(self, client, mock_celery, mock_user_auth):
- resp = client.get("/task-status/some-task-id")
- assert resp.status_code == 200
- body = resp.json()
- assert body["task_id"] == "some-task-id"
- assert body["state"] == "PENDING"
- assert body["result"] is None
-
- assert mock_celery["async_result_calls"] == ["some-task-id"]
-
- def test_task_status_success(self, client, mock_celery, mock_user_auth):
- fake_result_class = type(mock_celery["async_result_return"])
- mock_celery["async_result_return"] = fake_result_class(
- state="SUCCESS", result={"accuracy": 0.97}
- )
-
- resp = client.get("/task-status/finished-task")
- assert resp.status_code == 200
- body = resp.json()
- assert body["state"] == "SUCCESS"
- assert body["result"] == {"accuracy": 0.97}
-
- def test_task_status_failure(self, client, mock_celery, mock_user_auth):
- fake_result_class = type(mock_celery["async_result_return"])
- mock_celery["async_result_return"] = fake_result_class(
- state="FAILURE", result="boom: division by zero"
- )
-
- resp = client.get("/task-status/broken-task")
- assert resp.status_code == 200
- body = resp.json()
- assert body["state"] == "FAILURE"
- assert body["result"] == "boom: division by zero"
-
-
-class TestAnomalies:
- def _make_anomaly(self, device, timestamp, score=0.9):
- return Anomaly(
- metric_id=1,
- cpu=90.0,
- ram=80.0,
- timestamp=timestamp,
- reason="cpu_spike",
- score=score,
- device=device,
- )
-
- def test_get_anomalies_default_window(self, client, db_session, mock_user_auth):
- now = datetime.now(UTC)
- db_session.add_all(
- [
- self._make_anomaly("srv-1", now - timedelta(hours=1)),
- self._make_anomaly("srv-1", now - timedelta(hours=48)),
- ]
- )
- db_session.commit()
-
- resp = client.get("/anomalies")
- assert resp.status_code == 200
- body = resp.json()
- assert len(body) == 3
- assert body["items"][0]["device"] == "srv-1"
-
- def test_get_anomalies_filter_by_device(self, client, db_session, mock_user_auth):
- now = datetime.now(UTC)
- db_session.add_all(
- [
- self._make_anomaly("srv-1", now),
- self._make_anomaly("srv-2", now),
- ]
- )
- db_session.commit()
-
- resp = client.get("/anomalies", params={"device": "srv-2"})
- assert resp.status_code == 200
- body = resp.json()
- assert len(body) == 3
- assert body["items"][0]["device"] == "srv-2"
-
- def test_get_anomalies_hours_zero_disables_time_filter(
- self, client, db_session, mock_user_auth
- ):
- now = datetime.now(UTC)
- db_session.add(self._make_anomaly("srv-1", now - timedelta(days=30)))
- db_session.commit()
-
- resp = client.get("/anomalies", params={"hours": 0})
- assert resp.status_code == 200
- assert len(resp.json()) == 3
-
- def test_get_anomalies_ordered_desc(self, client, db_session, mock_user_auth):
- now = datetime.now(UTC)
- db_session.add_all(
- [
- self._make_anomaly("srv-1", now - timedelta(minutes=10)),
- self._make_anomaly("srv-1", now - timedelta(minutes=1)),
- ]
- )
- db_session.commit()
-
- resp = client.get("/anomalies")
- body = resp.json()
- timestamps = [a["timestamp"] for a in body["items"]]
- assert timestamps == sorted(timestamps, reverse=True)
-
-
-class TestPrometheus:
- def test_prometheus_targets_only_active_with_ip(self, client, db_session):
- db_session.add_all(
- [
- Device(ip="10.0.0.1", name="active-1", is_active=True),
- Device(ip="10.0.0.2", name="inactive-1", is_active=False),
- ]
- )
- db_session.commit()
-
- resp = client.get("/api/prometheus/targets")
- assert resp.status_code == 200
- body = resp.json()
-
- assert len(body) == 1
- target = body[0]
- assert target["targets"] == ["10.0.0.1:9100"]
- assert target["labels"] == {
- "job": "active-1",
- "physical_pc": "true",
- "device_label": "active-1",
- }
-
-
-class TestAuth:
- def test_valid_login(self, client, db_session, mock_redis):
- admin = Users(username="admin")
- admin.set_password("123456")
-
- db_session.add(admin)
- db_session.commit()
-
- resp = client.post(
- "api/auth/login", json={"username": "admin", "password": "123456"}
- )
- assert resp.status_code == 200
- assert resp.json()["status"] == "ok"
-
- assert "user_session" in resp.cookies
-
- def test_invalid_login(self, client, db_session, mock_redis):
- admin = Users(username="admin")
- admin.set_password("password123")
-
- db_session.add(admin)
- db_session.commit()
-
- resp = client.post(
- "api/auth/login", json={"username": "admin", "password": "123456"}
- )
- assert resp.status_code == 401
- assert resp.json()["detail"] == "Invalid username or password"
-
- def test_auth_me(self, client, mock_user_auth):
- resp = client.get("/api/auth/me")
-
- assert resp.status_code == 200
- assert resp.json() == {"authenticated": True}
-
- def test_valid_logout(self, client, mock_redis):
- client.cookies.set("admin_session", "session123")
-
- resp = client.post(
- "/api/auth/logout",
- )
-
- assert resp.status_code == 200
- assert resp.json()["status"] == "ok"
-
- def test_password_hash(self):
- admin = Users(username="admin")
-
- admin.set_password("123456")
-
- assert admin.password_hash != "123456"
- assert admin.check_password("123456")
- assert not admin.check_password("qwerty")
-
- def test_password_hash_random_salt(self):
- a = Users(username="a")
- b = Users(username="b")
-
- a.set_password("123456")
- b.set_password("123456")
-
- assert a.password_hash != b.password_hash
-
- @pytest.mark.parametrize(
- "method, endpoint",
- [
- ("POST", "/devices"),
- ("DELETE", "/devices/1"),
- ("POST", "/train?hours=1"),
- ("DELETE", "/model"),
- ],
- )
- def test_admin_previliges_with_user(self, client, method, endpoint, mock_user_auth):
- resp = client.request(method, endpoint)
-
- assert resp.status_code == 403
-
- @pytest.mark.parametrize(
- "method, endpoint",
- [
- ("GET", "/devices"),
- ("GET", "/anomalies"),
- ("GET", "/model-info"),
- ("GET", "/api/dashboard"),
- ("GET", "/db-metrics"),
- ("GET", "/task-status/dummy-task-id-123"),
- ],
- )
- def test_user_previliges_with_user(
- self, client, method, endpoint, mock_user_auth, mock_celery
- ):
- resp = client.request(method, endpoint)
-
- assert resp.status_code == 200
-
- @pytest.mark.parametrize(
- "method, endpoint",
- [
- ("GET", "/devices"),
- ("GET", "/anomalies"),
- ("GET", "/model-info"),
- ("GET", "/api/dashboard"),
- ("GET", "/db-metrics"),
- ("GET", "/task-status/dummy-task-id-123"),
- ],
- )
- def test_user_previliges_with_admin(
- self, client, method, endpoint, mock_admin_auth, mock_celery
- ):
- resp = client.request(method, endpoint)
-
- assert resp.status_code == 200
-
-
-class TestSecureEndpoints:
- @pytest.mark.parametrize(
- "method, endpoint",
- [
- # Auth
- ("GET", "/api/auth/me"),
- # Devices
- ("GET", "/devices"),
- ("POST", "/devices"),
- ("DELETE", "/devices/1"),
- # Model & Anomalies
- ("GET", "/anomalies"),
- ("GET", "/model-info"),
- ("POST", "/train?hours=1"),
- ("DELETE", "/model"),
- # Dashboard & Metrics
- ("GET", "/api/dashboard"),
- ("GET", "/db-metrics"),
- # Tasks
- ("GET", "/task-status/dummy-task-id-123"),
- ],
- )
- def test_endpoints_without_cookie_return_401(self, client, method, endpoint):
- resp = client.request(method, endpoint)
-
- assert resp.status_code == 401
-
-class TestRateLimiter:
- def test_login_rate_limit_exceeded(self, client, db_session, mock_redis):
- admin = Users(username="admin")
- admin.set_password("correct_password")
- db_session.add(admin)
- db_session.commit()
-
- responses = []
-
- for _ in range(10):
- resp = client.post(
- "/api/auth/login",
- json={"username": "admin", "password": "wrong_password"}
- )
- responses.append(resp)
-
- assert any(r.status_code == 429 for r in responses)
-
- last_resp = responses[-1]
- assert last_resp.status_code == 429
-
- def test_successful_login_resets_rate_limit(self, client, db_session, mock_redis):
- admin = Users(username="admin")
- admin.set_password("123456")
- db_session.add(admin)
- db_session.commit()
-
- for _ in range(2):
- client.post(
- "/api/auth/login",
- json={"username": "admin", "password": "wrong_password"}
- )
-
- assert any("user:admin" in key or "ip:" in key for key in mock_redis.storage)
-
- resp = client.post(
- "/api/auth/login",
- json={"username": "admin", "password": "123456"}
- )
- assert resp.status_code == 200
-
- assert mock_redis.storage.get("user:admin") is None
-
- def test_rate_limit_per_user(self, client, db_session, mock_redis):
- mock_redis.storage["login_attempts:user:target_user"] = 5
-
- resp = client.post(
- "/api/auth/login",
- json={"username": "target_user", "password": "some_password"}
- )
-
- assert resp.status_code == 429
\ No newline at end of file
diff --git a/tests/test_auth.py b/tests/test_auth.py
new file mode 100644
index 0000000..842deab
--- /dev/null
+++ b/tests/test_auth.py
@@ -0,0 +1,309 @@
+import pytest
+
+from mona_core.db import Users
+
+class TestAuth:
+ def test_valid_login(self, client, db_session, mock_redis):
+ admin = Users(username="admin")
+ admin.set_password("123456")
+
+ db_session.add(admin)
+ db_session.commit()
+
+ resp = client.post(
+ "/api/v1/auth/login", json={"username": "admin", "password": "123456"}
+ )
+ assert resp.status_code == 200
+ assert resp.json()["status"] == "ok"
+
+ assert "user_session" in resp.cookies
+
+ def test_invalid_login(self, client, db_session, mock_redis):
+ admin = Users(username="admin")
+ admin.set_password("password123")
+
+ db_session.add(admin)
+ db_session.commit()
+
+ resp = client.post(
+ "/api/v1/auth/login", json={"username": "admin", "password": "123456"}
+ )
+ assert resp.status_code == 401
+ assert resp.json()["message"] == "Invalid username or password"
+
+ def test_auth_me(self, client, mock_user_auth):
+ resp = client.get("/api/v1/auth/me")
+
+ assert resp.status_code == 200
+ assert resp.json() == {"authenticated": True}
+
+ def test_valid_logout(self, client, mock_redis):
+ client.cookies.set("admin_session", "session123")
+
+ resp = client.post(
+ "/api/v1/auth/logout",
+ )
+
+ assert resp.status_code == 200
+ assert resp.json()["status"] == "ok"
+
+ def test_password_hash(self):
+ admin = Users(username="admin")
+
+ admin.set_password("123456")
+
+ assert admin.password_hash != "123456"
+ assert admin.check_password("123456")
+ assert not admin.check_password("qwerty")
+
+ def test_password_hash_random_salt(self):
+ a = Users(username="a")
+ b = Users(username="b")
+
+ a.set_password("123456")
+ b.set_password("123456")
+
+ assert a.password_hash != b.password_hash
+
+ @pytest.mark.parametrize(
+ "method, endpoint",
+ [
+ ("POST", "/api/v1/devices"),
+ ("DELETE", "/api/v1/devices/1"),
+ ("POST", "/api/v1/train?hours=1"),
+ ("DELETE", "/api/v1/model"),
+ ],
+ )
+ def test_admin_previliges_with_user(self, client, method, endpoint, mock_user_auth):
+ resp = client.request(method, endpoint)
+
+ assert resp.status_code == 403
+
+ @pytest.mark.parametrize(
+ "method, endpoint",
+ [
+ ("GET", "/api/v1/devices"),
+ ("GET", "/api/v1/anomalies"),
+ ("GET", "/api/v1/model-info"),
+ ("GET", "/api/v1/dashboard"),
+ ("GET", "/api/v1/db-metrics"),
+ ("GET", "/api/v1/task-status/dummy-task-id-123"),
+ ],
+ )
+ def test_user_previliges_with_user(
+ self, client, method, endpoint, mock_user_auth, mock_celery
+ ):
+ resp = client.request(method, endpoint)
+
+ assert resp.status_code == 200
+
+ @pytest.mark.parametrize(
+ "method, endpoint",
+ [
+ ("GET", "/api/v1/devices"),
+ ("GET", "/api/v1/anomalies"),
+ ("GET", "/api/v1/model-info"),
+ ("GET", "/api/v1/dashboard"),
+ ("GET", "/api/v1/db-metrics"),
+ ("GET", "/api/v1/task-status/dummy-task-id-123"),
+ ],
+ )
+ def test_user_previliges_with_admin(
+ self, client, method, endpoint, mock_admin_auth, mock_celery
+ ):
+ resp = client.request(method, endpoint)
+
+ assert resp.status_code == 200
+
+
+class TestSecureEndpoints:
+ @pytest.mark.parametrize(
+ "method, endpoint",
+ [
+ # Auth
+ ("GET", "/api/v1/auth/me"),
+ # Devices
+ ("GET", "/api/v1/devices"),
+ ("POST", "/api/v1/devices"),
+ ("DELETE", "/api/v1/devices/1"),
+ # Model & Anomalies
+ ("GET", "/api/v1/anomalies"),
+ ("GET", "/api/v1/model-info"),
+ ("POST", "/api/v1/train?hours=1"),
+ ("DELETE", "/api/v1/model"),
+ # Dashboard & Metrics
+ ("GET", "/api/v1/dashboard"),
+ ("GET", "/api/v1/db-metrics"),
+ # Tasks
+ ("GET", "/api/v1/task-status/dummy-task-id-123"),
+ ],
+ )
+ def test_endpoints_without_cookie_return_401(self, client, method, endpoint):
+ resp = client.request(method, endpoint)
+
+ assert resp.status_code == 401
+
+class TestRateLimiter:
+ def test_login_rate_limit_exceeded(self, client, db_session, mock_redis):
+ admin = Users(username="admin")
+ admin.set_password("correct_password")
+ db_session.add(admin)
+ db_session.commit()
+
+ responses = []
+
+ for _ in range(10):
+ resp = client.post(
+ "/api/v1/auth/login",
+ json={"username": "admin", "password": "wrong_password"}
+ )
+ responses.append(resp)
+
+ assert any(r.status_code == 429 for r in responses)
+
+ last_resp = responses[-1]
+ assert last_resp.status_code == 429
+
+ @pytest.mark.asyncio
+ async def test_successful_login_resets_rate_limit(self, client, db_session, mock_redis):
+ admin = Users(username="admin")
+ admin.set_password("123456")
+ db_session.add(admin)
+ db_session.commit()
+
+ for _ in range(2):
+ client.post(
+ "/api/v1/auth/login",
+ json={"username": "admin", "password": "wrong_password"}
+ )
+
+ keys = await mock_redis.keys("login_attempts:*")
+ assert any("user:admin" in k or "ip:" in k for k in keys)
+
+ resp = client.post(
+ "/api/v1/auth/login",
+ json={"username": "admin", "password": "123456"}
+ )
+ assert resp.status_code == 200
+
+ assert await mock_redis.get("login_attempts:user:admin") is None
+
+ @pytest.mark.asyncio
+ async def test_rate_limit_per_user(self, client, db_session, mock_redis):
+ await mock_redis.set("login_attempts:user:target_user", 5)
+
+ resp = client.post(
+ "/api/v1/auth/login",
+ json={"username": "target_user", "password": "some_password"}
+ )
+
+ assert resp.status_code == 429
+
+class TestValidation:
+ @pytest.mark.parametrize(
+ "ip, name",
+ [
+ ("10.0.0", "test"),
+ ("10", "test-10"),
+ ("test", "srv-10"),
+ ("2001:db8::1::2", "pc-12"),
+ ("gggg::1", "srv-ipv6"),
+ ("12345::1", "test-ipv6"),
+ ],
+ )
+ def test_create_device_invalid_ip(self, client, ip, name, mock_admin_auth):
+ payload = {"ip": ip, "name": name, "is_active": True}
+ resp = client.post("/api/v1/devices", json=payload)
+
+ assert resp.status_code == 422
+ assert (
+ resp.json()["detail"][0]["msg"]
+ == "Value error, Must be a valid IPv4 or IPv6 address"
+ )
+
+ @pytest.mark.parametrize(
+ "ip, name",
+ [
+ ("172.16.0.5", "{1==1}"),
+ ("10.0.1.10", "bad_u$er}"),
+ ("192.168.0.10", "} job {"),
+ ("::1", "pc 12"),
+ ("2001:db8::ff00:42:8329", "1234567890101112"),
+ ("2001:0db8:0000:0000:0000:8a2e:0370:7334", "SRV-123456789101111"),
+ ],
+ )
+ def test_create_device_invalid_name(self, ip, name, client, mock_admin_auth):
+ payload = {"ip": ip, "name": name, "is_active": True}
+ resp = client.post("/api/v1/devices", json=payload)
+
+ assert resp.status_code == 422
+ assert (
+ resp.json()["detail"][0]["msg"]
+ == "Value error, Name can only contain letters, numbers, '_' and '-' (up to 15 characters)"
+ )
+
+ @pytest.mark.parametrize(
+ "ip, name",
+ [
+ ("192.168.1.20", "a" * 15),
+ ("192.168.1.21", "a"),
+ ("192.168.1.22", "srv_01-test"),
+ ("2001:db8::1", "ipv6-device"),
+ ],
+ )
+ def test_create_device_valid_name(self, client, ip, name, mock_admin_auth):
+ resp = client.post("/api/v1/devices", json={"ip": ip, "name": name, "is_active": True})
+ assert resp.status_code == 201
+
+ listed = client.get("/api/v1/devices").json()
+ assert any(d["name"] == name for d in listed)
+
+ def test_create_device_ip_leading_zeros_rejected(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/devices",
+ json={"ip": "192.168.001.001", "name": "test", "is_active": True},
+ )
+ assert resp.status_code == 422
+
+ def test_create_device_ip_cidr_rejected(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/devices", json={"ip": "10.0.0.1/24", "name": "test", "is_active": True}
+ )
+ assert resp.status_code == 422
+
+ def test_create_device_trims_whitespace(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/devices",
+ json={
+ "ip": " 192.168.1.30 ",
+ "name": " trimmed-name ",
+ "is_active": True,
+ },
+ )
+ assert resp.status_code == 201
+
+ listed = client.get("/api/v1/devices").json()
+ dev = next(d for d in listed if d["name"] == "trimmed-name")
+ assert dev["ip"] == "192.168.1.30"
+
+ def test_create_device_missing_name(self, client, mock_admin_auth):
+ resp = client.post("/api/v1/devices", json={"ip": "192.168.1.40", "is_active": True})
+ assert resp.status_code == 422
+ assert resp.json()["detail"][0]["type"] == "missing"
+
+ def test_create_device_name_wrong_type(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/devices", json={"ip": "192.168.1.41", "name": 12345, "is_active": True}
+ )
+ assert resp.status_code == 422
+
+ def test_create_device_ipv4_mapped_ipv6(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/devices",
+ json={"ip": "::ffff:192.168.1.1", "name": "mapped-ipv6", "is_active": True},
+ )
+ assert resp.status_code == 201
+
+ listed = client.get("/api/v1/devices").json()
+ dev = next(d for d in listed if d["name"] == "mapped-ipv6")
+ assert dev["ip"] == "::ffff:192.168.1.1"
diff --git a/tests/test_dashboard_metrics.py b/tests/test_dashboard_metrics.py
new file mode 100644
index 0000000..6710830
--- /dev/null
+++ b/tests/test_dashboard_metrics.py
@@ -0,0 +1,60 @@
+from datetime import UTC, datetime, timedelta
+
+from mona_core.db import Anomaly, Metric
+
+class TestDashboardAndMetrics:
+ def test_db_metrics_empty(self, client, mock_user_auth):
+ resp = client.get("/api/v1/db-metrics")
+ assert resp.status_code == 200
+ assert resp.json() == {"items": [], "next_cursor": None}
+
+ def test_db_metrics_filter_by_device(self, client, db_session, mock_user_auth):
+ db_session.add_all(
+ [
+ Metric(cpu=10, ram=20, device="srv-1"),
+ Metric(cpu=30, ram=40, device="srv-2"),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/db-metrics", params={"device": "srv-1"})
+ body = resp.json()
+ assert len(body) == 2
+ assert body["items"][0]["device"] == "srv-1"
+
+ def test_dashboard_combines_metrics_anomalies_and_model(
+ self, client, db_session, mock_user_auth
+ ):
+ now = datetime.now(UTC)
+ db_session.add_all(
+ [
+ Metric(cpu=50, ram=60, device="srv-1", timestamp=now),
+ Anomaly(
+ metric_id=1,
+ cpu=95,
+ ram=90,
+ timestamp=now,
+ reason="cpu_spike",
+ score=0.99,
+ device="srv-1",
+ ),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/dashboard", params={"hours": 1})
+ assert resp.status_code == 200
+ body = resp.json()
+
+ assert "srv-1" in body["devices"]
+ assert len(body["metrics"]) == 1
+ assert len(body["anomalies"]) == 1
+ assert body["model"] is None
+
+ def test_dashboard_respects_time_window(self, client, db_session, mock_user_auth):
+ old = datetime.now(UTC) - timedelta(hours=5)
+ db_session.add(Metric(cpu=1, ram=1, device="srv-1", timestamp=old))
+ db_session.commit()
+
+ resp = client.get("/api/v1/dashboard", params={"hours": 1})
+ assert resp.json()["metrics"] == []
diff --git a/tests/test_devices.py b/tests/test_devices.py
new file mode 100644
index 0000000..d302ce3
--- /dev/null
+++ b/tests/test_devices.py
@@ -0,0 +1,64 @@
+from mona_core.db import Device
+
+class TestDevices:
+ def test_get_device_empty(self, client, mock_user_auth):
+ resp = client.get("/api/v1/devices")
+ assert resp.status_code == 200
+ assert resp.json() == []
+
+ def test_create_device(self, client, mock_admin_auth):
+ payload = {"ip": "192.168.1.10", "name": "office-pc-1", "is_active": True}
+ resp = client.post("/api/v1/devices", json=payload)
+
+ assert resp.status_code == 201
+ assert resp.json() == {"status": "ok", "message": "Device created"}
+
+ listed = client.get("/api/v1/devices").json()
+ assert len(listed) == 1
+ assert listed[0]["ip"] == payload["ip"]
+ assert listed[0]["name"] == payload["name"]
+ assert listed[0]["is_active"] is True
+
+ def test_create_device_default_active(self, client, mock_admin_auth):
+ payload = {"ip": "192.168.1.10", "name": "office-pc-1"}
+ resp = client.post("/api/v1/devices", json=payload)
+ assert resp.status_code == 201
+
+ listed = client.get("/api/v1/devices").json()
+ assert listed[0]["is_active"] is True
+
+ def test_create_device_duplicate_name_conflict(self, client, mock_admin_auth):
+ payload = {"ip": "10.0.0.1", "name": "dup-name"}
+ first = client.post("/api/v1/devices", json=payload)
+ assert first.status_code == 201
+
+ second = client.post("/api/v1/devices", json={"ip": "10.0.0.2", "name": "dup-name"})
+ assert second.status_code == 409
+ assert second.json()["message"] == "Name already exists"
+
+ def test_list_devices_after_create(self, client, mock_admin_auth):
+ client.post("/api/v1/devices", json={"ip": "10.0.0.1", "name": "a"})
+ client.post("/api/v1/devices", json={"ip": "10.0.0.2", "name": "b"})
+
+ resp = client.get("/api/v1/devices")
+ assert resp.status_code == 200
+ names = {d["name"] for d in resp.json()}
+ assert names == {"a", "b"}
+
+ def test_delete_device_success(self, client, db_session, mock_admin_auth):
+ dev = Device(ip="10.0.0.9", name="to-delete")
+ db_session.add(dev)
+ db_session.commit()
+ db_session.refresh(dev)
+
+ resp = client.delete(f"/api/v1/devices/{dev.id}")
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "Device deleted"}
+
+ resp2 = client.get("/api/v1/devices")
+ assert resp2.json() == []
+
+ def test_delete_device_not_found(self, client, mock_admin_auth):
+ resp = client.delete("/api/v1/devices/99999")
+ assert resp.status_code == 404
+ assert resp.json()["message"] == "Device not found"
diff --git a/tests/test_ml.py b/tests/test_ml.py
index 850a2e8..f99b9d2 100644
--- a/tests/test_ml.py
+++ b/tests/test_ml.py
@@ -331,4 +331,4 @@ def test_error_returns_error_status(
result = ml.detect_anomalies()
assert result["status"] == "error"
- assert "broken" in result["error"]
+ assert "Server error" in result["error"]
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..d09acfb
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,76 @@
+from datetime import UTC, datetime
+
+from mona_core.db import TrainedModel
+
+class TestModel:
+ def test_model_info_no_model(self, client, mock_user_auth):
+ resp = client.get("/api/v1/model-info")
+ assert resp.status_code == 200
+ assert resp.json() == {
+ "status": "no_model",
+ "message": "Model is not manually trained yet. Using auto-mode.",
+ }
+
+ def test_model_info_with_model(self, client, db_session, mock_user_auth):
+ db_session.add(
+ TrainedModel(
+ model_data=b"binary-blob",
+ trained_by="user",
+ points_count=1000,
+ period_from=datetime(2026, 1, 1, tzinfo=UTC),
+ period_to=datetime(2026, 1, 2, tzinfo=UTC),
+ note="nightly training",
+ )
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/model-info")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["status"] == "ok"
+ assert body["model"]["points_count"] == 1000
+ assert body["model"]["note"] == "nightly training"
+
+ def test_model_info_ignores_non_user_trained(
+ self, client, db_session, mock_user_auth
+ ):
+ db_session.add(
+ TrainedModel(
+ model_data=b"x",
+ trained_by="auto",
+ points_count=1,
+ period_from=datetime(2026, 1, 1, tzinfo=UTC),
+ period_to=datetime(2026, 1, 2, tzinfo=UTC),
+ note="",
+ )
+ )
+ db_session.commit()
+
+ resp = client.get("/api/v1/model-info")
+ assert resp.json()["status"] == "no_model"
+
+ def test_delete_model(self, client, db_session, mock_admin_auth):
+ db_session.add(
+ TrainedModel(
+ model_data=b"x",
+ trained_by="user",
+ points_count=1,
+ period_from=datetime(2026, 1, 1, tzinfo=UTC),
+ period_to=datetime(2026, 1, 2, tzinfo=UTC),
+ note="",
+ )
+ )
+ db_session.commit()
+
+ resp = client.delete("/api/v1/model")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["status"] == "ok"
+ assert body["deleted"] == 1
+
+ assert client.get("/api/v1/model-info").json()["status"] == "no_model"
+
+ def test_delete_model_when_none_exists(self, client, mock_admin_auth):
+ resp = client.delete("/api/v1/model")
+ assert resp.status_code == 200
+ assert resp.json()["deleted"] == 0
\ No newline at end of file
diff --git a/tests/test_probes.py b/tests/test_probes.py
new file mode 100644
index 0000000..8a84509
--- /dev/null
+++ b/tests/test_probes.py
@@ -0,0 +1,23 @@
+
+class TestProbes:
+ def test_liveness_probe(self, client):
+ resp = client.get("/health/live")
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "alive"}
+
+ def test_readiness_probe_ok(self, client, mock_redis):
+ resp = client.get("/health/ready")
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ready"}
+
+ def test_readiness_probe_db_down(self, client, monkeypatch):
+ from sqlalchemy.orm import Session
+
+ def broken_execute(self, *args, **kwargs):
+ raise Exception("connection refused")
+
+ monkeypatch.setattr(Session, "execute", broken_execute)
+
+ resp = client.get("/health/ready")
+ assert resp.status_code == 503
+ assert resp.json()["message"] == "Database unavailable"
diff --git a/tests/test_prometheus.py b/tests/test_prometheus.py
new file mode 100644
index 0000000..2936f89
--- /dev/null
+++ b/tests/test_prometheus.py
@@ -0,0 +1,24 @@
+from mona_core.db import Device
+
+class TestPrometheus:
+ def test_prometheus_targets_only_active_with_ip(self, client, db_session):
+ db_session.add_all(
+ [
+ Device(ip="10.0.0.1", name="active-1", is_active=True),
+ Device(ip="10.0.0.2", name="inactive-1", is_active=False),
+ ]
+ )
+ db_session.commit()
+
+ resp = client.get("/api/prometheus/targets")
+ assert resp.status_code == 200
+ body = resp.json()
+
+ assert len(body) == 1
+ target = body[0]
+ assert target["targets"] == ["10.0.0.1:9100"]
+ assert target["labels"] == {
+ "job": "active-1",
+ "physical_pc": "true",
+ "device_label": "active-1",
+ }
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 38a4010..b4e607a 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -55,10 +55,13 @@ def test_build_features_delta(self):
class TestQuery:
def test_query_returns_value(self, monkeypatch):
class FakeResponse:
+ def raise_for_status(self):
+ pass
+
def json(self):
return {"data": {"result": [{"value": [123, "57.8"]}]}}
- monkeypatch.setattr(tasks.requests, "get", lambda *a, **k: FakeResponse())
+ monkeypatch.setattr(tasks.httpx2, "get", lambda *a, **k: FakeResponse())
value = tasks._query("http://localhost", "up")
@@ -66,10 +69,13 @@ def json(self):
def test_query_empty_result(self, monkeypatch):
class FakeResponse:
+ def raise_for_status(self):
+ pass
+
def json(self):
return {"data": {"result": []}}
- monkeypatch.setattr(tasks.requests, "get", lambda *a, **k: FakeResponse())
+ monkeypatch.setattr(tasks.httpx2, "get", lambda *a, **k: FakeResponse())
assert tasks._query("url", "query") == 0
@@ -79,17 +85,14 @@ def test_collect_creates_device(
self,
monkeypatch,
db_session,
+ patch_prometheus_client,
):
monkeypatch.setenv(
"EXPORTERS",
"pc1:10.0.0.1",
)
- monkeypatch.setattr(
- tasks,
- "_query",
- lambda *a, **k: 25.0,
- )
+ patch_prometheus_client([25.0, 25.0])
result = tasks.collect_and_save()
@@ -104,6 +107,7 @@ def test_collect_updates_ip(
self,
monkeypatch,
db_session,
+ patch_prometheus_client,
):
db_session.add(
Device(
@@ -119,11 +123,7 @@ def test_collect_updates_ip(
"pc1:2.2.2.2",
)
- monkeypatch.setattr(
- tasks,
- "_query",
- lambda *a, **k: 10,
- )
+ patch_prometheus_client([10, 10])
tasks.collect_and_save()
@@ -149,6 +149,7 @@ def test_collect_saves_metric(
self,
monkeypatch,
db_session,
+ patch_prometheus_client,
):
db_session.add(
Device(
@@ -159,13 +160,7 @@ def test_collect_saves_metric(
)
db_session.commit()
- values = iter([55.5, 77.7])
-
- monkeypatch.setattr(
- tasks,
- "_query",
- lambda *a, **k: next(values),
- )
+ patch_prometheus_client([55.5, 77.7])
result = tasks.collect_and_save()
@@ -263,4 +258,59 @@ def test_load_failure(
)
assert result["status"] == "error"
- assert "skops error" in result["message"]
+ assert "Server error" in result["message"]
+
+class TestTasksApi:
+ def test_train_model_submits_task(self, client, mock_celery, mock_admin_auth):
+ resp = client.post("/api/v1/train", params={"hours": 2.5, "note": "manual run"})
+
+ assert resp.status_code == 202
+ body = resp.json()
+ assert body["status"] == "accepted"
+ assert "task_id" in body
+
+ assert len(mock_celery["send_task_calls"]) == 1
+ call = mock_celery["send_task_calls"][0]
+ assert call["name"] == "tasks.train_model_task"
+ assert call["kwargs"] == {"hours": 2.5, "note": "manual run"}
+
+ def test_train_model_defaults(self, client, mock_celery, mock_admin_auth):
+ resp = client.post("/api/v1/train")
+ assert resp.status_code == 202
+ assert len(mock_celery["send_task_calls"]) == 1
+ call = mock_celery["send_task_calls"][0]
+ assert call["name"] == "tasks.train_model_task"
+
+ def test_task_status_pending(self, client, mock_celery, mock_user_auth):
+ resp = client.get("/api/v1/task-status/some-task-id")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["task_id"] == "some-task-id"
+ assert body["state"] == "PENDING"
+ assert body["result"] is None
+
+ assert mock_celery["async_result_calls"] == ["some-task-id"]
+
+ def test_task_status_success(self, client, mock_celery, mock_user_auth):
+ fake_result_class = type(mock_celery["async_result_return"])
+ mock_celery["async_result_return"] = fake_result_class(
+ state="SUCCESS", result={"accuracy": 0.97}
+ )
+
+ resp = client.get("/api/v1/task-status/finished-task")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["state"] == "SUCCESS"
+ assert body["result"] == {"accuracy": 0.97}
+
+ def test_task_status_failure(self, client, mock_celery, mock_user_auth):
+ fake_result_class = type(mock_celery["async_result_return"])
+ mock_celery["async_result_return"] = fake_result_class(
+ state="FAILURE", result="boom: division by zero"
+ )
+
+ resp = client.get("/api/v1/task-status/broken-task")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["state"] == "FAILURE"
+ assert body["result"] == "boom: division by zero"
\ No newline at end of file
diff --git a/tests/test_users.py b/tests/test_users.py
new file mode 100644
index 0000000..47cc9d9
--- /dev/null
+++ b/tests/test_users.py
@@ -0,0 +1,209 @@
+from sqlalchemy import select
+
+from mona_core.db import Users
+
+class TestUser:
+ # ─── GET /api/v1/users ──────────────────────────────────────────────
+ def test_get_users_requires_auth(self, client):
+ resp = client.get("/api/v1/users")
+ assert resp.status_code == 401
+
+ def test_get_users_forbidden_for_regular_user(self, client, mock_user_auth):
+ resp = client.get("/api/v1/users")
+ assert resp.status_code == 403
+
+ def test_get_users_empty_list(self, client, mock_admin_auth):
+ resp = client.get("/api/v1/users")
+ assert resp.status_code == 200
+ assert resp.json() == []
+
+ def test_get_users_returns_created_users(self, client, mock_admin_auth, make_user):
+ make_user(username="alice", role="user")
+ make_user(username="bob", role="admin")
+
+ resp = client.get("/api/v1/users")
+ assert resp.status_code == 200
+
+ body = resp.json()
+ assert {u["username"] for u in body} == {"alice", "bob"}
+ for u in body:
+ # response_model=UserGetOut must never leak the password hash
+ assert "password" not in u
+ assert "password_hash" not in u
+
+ def test_get_users_pagination(self, client, mock_admin_auth, make_user):
+ for i in range(5):
+ make_user(username=f"user{i}", role="user")
+
+ resp = client.get("/api/v1/users", params={"limit": 2, "offset": 0})
+ assert resp.status_code == 200
+ assert len(resp.json()) == 2
+
+ resp2 = client.get("/api/v1/users", params={"limit": 2, "offset": 4})
+ assert resp2.status_code == 200
+ assert len(resp2.json()) == 1
+
+ # ─── POST /api/v1/user ──────────────────────────────────────────────
+ def test_create_user_requires_admin(self, client, mock_user_auth):
+ resp = client.post(
+ "/api/v1/user",
+ json={"username": "new_user", "password": "pass1234", "role": "user"},
+ )
+ assert resp.status_code == 403
+
+ def test_create_user_success(self, client, mock_admin_auth, db_session):
+ resp = client.post(
+ "/api/v1/user",
+ json={"username": "newadmin", "password": "supersecret", "role": "admin"},
+ )
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "User created"}
+
+ user = db_session.execute(
+ select(Users).where(Users.username == "newadmin")
+ ).scalar_one()
+ assert user.role == "admin"
+ assert user.password_hash != "supersecret"
+ assert user.check_password("supersecret")
+
+ def test_create_user_duplicate_username(self, client, mock_admin_auth, make_user):
+ make_user(username="dup", role="user")
+
+ resp = client.post(
+ "/api/v1/user",
+ json={"username": "dup", "password": "whatever1", "role": "user"},
+ )
+ assert resp.status_code == 409
+ assert resp.json()["message"] == "User already exists"
+
+ def test_create_user_invalid_role_rejected(self, client, mock_admin_auth):
+ resp = client.post(
+ "/api/v1/user",
+ json={"username": "weird", "password": "whatever1", "role": "superadmin"},
+ )
+ assert resp.status_code == 422
+
+ # ─── DELETE /api/v1/user/{user_id} ─────────────────────────────────
+ def test_delete_user_requires_admin(self, client, mock_user_auth, make_user):
+ user = make_user(username="victim", role="user")
+ resp = client.delete(f"/api/v1/user/{user.id}")
+ assert resp.status_code == 403
+
+ def test_delete_user_not_found(self, client, mock_admin_auth, make_user):
+ # Two admins present so the "last admin" guard doesn't shadow the 404.
+ make_user(username="admin1", role="admin")
+ make_user(username="admin2", role="admin")
+
+ resp = client.delete("/api/v1/user/999999")
+ assert resp.status_code == 404
+ assert resp.json()["message"] == "User not found"
+
+ def test_delete_last_remaining_admin_forbidden(self, client, mock_admin_auth, make_user):
+ admin = make_user(username="lonely_admin", role="admin")
+
+ resp = client.delete(f"/api/v1/user/{admin.id}")
+ assert resp.status_code == 400
+ assert resp.json()["message"] == "Cannot delete the last remaining admin"
+
+ def test_delete_user_success(self, client, mock_admin_auth, make_user, db_session, mock_redis):
+ make_user(username="admin1", role="admin")
+ victim = make_user(username="victim", role="user")
+
+ resp = client.delete(f"/api/v1/user/{victim.id}")
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "User deleted"}
+
+ remaining = db_session.execute(
+ select(Users).where(Users.id == victim.id)
+ ).scalar_one_or_none()
+ assert remaining is None
+
+ # ─── PATCH /api/v1/user/change-password ────────────────────────────
+ def test_change_password_requires_admin(self, client, mock_user_auth, make_user):
+ make_user(username="alice", password="oldpass1")
+
+ resp = client.patch(
+ "/api/v1/user/change-password",
+ json={"username": "alice", "new_password": "newpass1"},
+ )
+ assert resp.status_code == 403
+
+ def test_change_password_user_not_found(self, client, mock_admin_auth):
+ resp = client.patch(
+ "/api/v1/user/change-password",
+ json={"username": "ghost", "new_password": "newpass1"},
+ )
+ assert resp.status_code == 404
+ assert resp.json()["message"] == "Username invalid"
+
+ def test_change_password_success(self, client, mock_admin_auth, make_user, db_session):
+ make_user(username="alice", password="oldpass1")
+
+ resp = client.patch(
+ "/api/v1/user/change-password",
+ json={"username": "alice", "new_password": "brandnewpass"},
+ )
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "Password changed"}
+
+ user = db_session.execute(
+ select(Users).where(Users.username == "alice")
+ ).scalar_one()
+ assert user.check_password("brandnewpass")
+ assert not user.check_password("oldpass1")
+
+ # ─── PATCH /api/v1/user/{user_id}/role ─────────────────────────────
+ def test_change_role_requires_admin(self, client, mock_user_auth, make_user):
+ target = make_user(username="alice", role="user")
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "admin"})
+ assert resp.status_code == 403
+
+ def test_change_role_target_not_found(self, client, mock_admin_auth):
+ resp = client.patch("/api/v1/user/999999/role", json={"role": "admin"})
+ assert resp.status_code == 404
+ assert resp.json()["message"] == "User not found"
+
+ def test_change_role_unchanged_is_noop(self, client, mock_admin_auth, make_user):
+ target = make_user(username="alice", role="user")
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "user"})
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "Role unchanged"}
+
+ def test_change_role_promote_to_admin(self, client, mock_admin_auth, make_user, db_session):
+ target = make_user(username="alice", role="user")
+
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "admin"})
+ assert resp.status_code == 200
+ assert resp.json() == {"status": "ok", "message": "Role switched"}
+
+ db_session.refresh(target)
+ assert target.role == "admin"
+
+ def test_change_role_demote_last_admin_forbidden(self, client, mock_admin_auth, make_user):
+ target = make_user(username="only_admin", role="admin")
+
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "user"})
+ assert resp.status_code == 400
+ assert resp.json()["message"] == "Cannot demote the last remaining admin"
+
+ def test_change_role_demote_when_multiple_admins_exist(
+ self, client, mock_admin_auth, make_user, db_session
+ ):
+ make_user(username="admin1", role="admin")
+ target = make_user(username="admin2", role="admin")
+
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "user"})
+ assert resp.status_code == 200
+
+ db_session.refresh(target)
+ assert target.role == "user"
+
+ def test_change_role_cannot_change_own_role(self, client, mock_admin_auth, make_user):
+ # The endpoint's own-account guard only fires on the "user -> admin"
+ # branch (see the if/elif chain in change_user_role): it compares the
+ # target row's username against the authenticated session's username.
+ target = make_user(username=mock_admin_auth.username, role="user")
+
+ resp = client.patch(f"/api/v1/user/{target.id}/role", json={"role": "admin"})
+ assert resp.status_code == 403
+ assert resp.json()["message"] == "You cannot change your own role"
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index 3255191..da0e935 100644
--- a/uv.lock
+++ b/uv.lock
@@ -30,7 +30,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.13.5"
+version = "3.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -41,59 +41,72 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
- { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
- { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
- { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
- { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
- { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
- { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
- { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
- { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
- { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
- { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
- { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
- { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
- { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
- { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
- { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
- { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
- { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
- { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
- { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
- { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
- { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
- { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
- { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
- { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
- { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
- { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
- { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
- { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
- { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
- { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
- { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
- { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
- { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
- { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
- { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
- { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
- { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
- { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
- { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
- { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
- { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
- { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
- { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
- { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
+ { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
+ { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
+ { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
+ { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
+ { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
+ { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
+ { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
+ { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
+ { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
+ { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
+ { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
+ { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
]
[[package]]
@@ -119,16 +132,16 @@ wheels = [
[[package]]
name = "alembic"
-version = "1.18.5"
+version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" },
+ { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" },
]
[[package]]
@@ -145,82 +158,105 @@ wheels = [
[[package]]
name = "annotated-doc"
-version = "0.0.4"
+version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]]
name = "annotated-types"
-version = "0.7.0"
+version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
]
[[package]]
name = "anyio"
-version = "4.14.1"
+version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
[[package]]
name = "argcomplete"
-version = "3.7.0"
+version = "3.7.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/c0/c8e94135e66fabf89a120d9b4b123fe6993506beca6c1938a74c24cfa5fd/argcomplete-3.7.0.tar.gz", hash = "sha256:afde224f753f874807b1dc1414e883ab8fe0cda9c04807b6047dcb8e1ac23913", size = 73284, upload-time = "2026-06-30T22:28:22.249Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/12/f6/5b8ec087cd9cfa9449491ec83f76fb6b7006b4dff57d2ba8aaab330fe8e4/argcomplete-3.7.0-py3-none-any.whl", hash = "sha256:d8f0f22d2a8a7caa383be1e22b6caf1ecaf0ebd10d8f83cc125e36540c95830c", size = 42575, upload-time = "2026-06-30T22:28:20.547Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" },
]
[[package]]
name = "ast-serialize"
-version = "0.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" },
- { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" },
- { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" },
- { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" },
- { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" },
- { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" },
- { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" },
- { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" },
- { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" },
- { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" },
- { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" },
- { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" },
- { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" },
- { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" },
- { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" },
- { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" },
- { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" },
- { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" },
- { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" },
- { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" },
- { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" },
- { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" },
- { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" },
- { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" },
- { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" },
- { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" },
- { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" },
- { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" },
- { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" },
- { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" },
- { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" },
+ { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" },
+ { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" },
+ { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" },
+ { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" },
+ { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" },
+ { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" },
+ { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" },
+ { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" },
+ { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" },
+ { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" },
+ { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" },
+ { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" },
+ { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" },
+ { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" },
+ { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" },
+ { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" },
]
[[package]]
@@ -243,16 +279,16 @@ wheels = [
[[package]]
name = "bc-detect-secrets"
-version = "1.5.47"
+version = "1.5.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "unidiff" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/82/fb/624aa462ea738cd21e56b1a5b7bbe375403e4114f7bc92a7cded7f516da0/bc_detect_secrets-1.5.47.tar.gz", hash = "sha256:a9be28a2e564f2b19731991df39e63ae6372cc84d828ee24e50c094cbb4c154c", size = 91339, upload-time = "2026-03-17T14:05:50.24Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/6d/ffa40dbb29321b45827329e26029a371f81463b803da76e99e9bf6f57e69/bc_detect_secrets-1.5.50.tar.gz", hash = "sha256:99037375d9cb49ed07e5bb12722f4bbb76fb8acaff6f367eef9df7559e7642b3", size = 99368, upload-time = "2026-08-19T11:21:38.82Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/54/6039515fe99f712801fe6dc6a67c4565ffd23a5b83fb1813b64c9fdb73a8/bc_detect_secrets-1.5.47-py3-none-any.whl", hash = "sha256:46f88c710b0fd8c5f2e54b361d793b5e1469197884da73cfc6f488b614366fc3", size = 121200, upload-time = "2026-03-17T14:05:49.218Z" },
+ { url = "https://files.pythonhosted.org/packages/52/4a/236b426c21d714edb6e399c7be81dca13d24c8b64411ab003d9499d59774/bc_detect_secrets-1.5.50-py3-none-any.whl", hash = "sha256:016ce9e79f692adbabcbef4a7293db427352911c3f81404a136e6f5c3e54a7f2", size = 126598, upload-time = "2026-08-19T11:21:37.69Z" },
]
[[package]]
@@ -454,137 +490,213 @@ wheels = [
[[package]]
name = "certifi"
-version = "2026.6.17"
+version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "cffi"
-version = "2.1.0"
+version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" },
- { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" },
- { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" },
- { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" },
- { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" },
- { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" },
- { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" },
- { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" },
- { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" },
- { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" },
- { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" },
- { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" },
- { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" },
- { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" },
- { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" },
- { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" },
- { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" },
- { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" },
- { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" },
- { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" },
- { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" },
- { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" },
- { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" },
- { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" },
- { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" },
- { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" },
- { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" },
- { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" },
- { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" },
- { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" },
- { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" },
- { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" },
- { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" },
- { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" },
- { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" },
- { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" },
- { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" },
- { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" },
- { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" },
- { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" },
- { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" },
- { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" },
- { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" },
- { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" },
- { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" },
- { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" },
- { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" },
- { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" },
- { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" },
- { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" },
- { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" },
- { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" },
- { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" },
- { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" },
- { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" },
- { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" },
- { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" },
- { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" },
- { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" },
- { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" },
- { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
+ { 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 = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "charset-normalizer"
-version = "3.4.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
- { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
- { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
- { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
- { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
- { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
- { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
- { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
- { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
- { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
- { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
- { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
- { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
- { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
- { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
- { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
- { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
- { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
- { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
- { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
- { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
- { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
- { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
- { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
- { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
- { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
- { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
- { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
- { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
- { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
- { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
- { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
- { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
- { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
- { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
- { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
- { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
- { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
- { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
+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/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" },
+ { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" },
+ { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" },
+ { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" },
+ { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" },
+ { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" },
+ { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" },
+ { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" },
+ { 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 = "checkov"
-version = "3.3.8"
+version = "3.3.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiodns" },
@@ -632,21 +744,18 @@ dependencies = [
{ name = "urllib3" },
{ name = "yarl" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ef/ad/1071f4a508975e7c50939419ada72c7c3987522860c8c53968e00c094f0c/checkov-3.3.8.tar.gz", hash = "sha256:a65e5cda0337e7770436eeadabb1f65c6ce4ad7f5b145a7f801bfdf14f1aea05", size = 1008775, upload-time = "2026-07-09T12:16:34.414Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/e1/f2cf8c90307c35e326e9eb736ffb4ff92921631d19c304d8956e13fd7b0c/checkov-3.3.15.tar.gz", hash = "sha256:382781a4d7308eefcd5b3f5c91e834f449806c50ed342193ddb8e4622534c5a7", size = 1009854, upload-time = "2026-08-26T10:50:05.174Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1c/13/aac27e3d79c4a69271b635cb62638dff77e7a816113743d82c883eb35fd4/checkov-3.3.8-py3-none-any.whl", hash = "sha256:8d59cd3bb06b9237868300602ba469687866077cc008fbc903404520acfa88a9", size = 2322517, upload-time = "2026-07-09T12:16:32.515Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/2b/29e225e631b1cf67d32a84657f6c7249dbbacee268c24c4e71bc453b6ffb/checkov-3.3.15-py3-none-any.whl", hash = "sha256:b410df22da3ed184aaac66901312634130ce259f915957e0a91e31005062e4b6", size = 2323302, upload-time = "2026-08-26T10:50:02.057Z" },
]
[[package]]
name = "click"
-version = "8.4.2"
+version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
+ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[package]]
@@ -779,6 +888,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
+[[package]]
+name = "distlib"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
+]
+
[[package]]
name = "docker"
version = "7.2.0"
@@ -802,17 +920,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" },
]
-[[package]]
-name = "dotenv"
-version = "0.9.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "python-dotenv" },
-]
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
-]
-
[[package]]
name = "dpath"
version = "2.1.3"
@@ -834,9 +941,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
]
+[[package]]
+name = "fakeredis"
+version = "2.37.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "redis" },
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/5e/fce3355d20d37dc18f7f4c422653ac69cabffa019afaaf607e26704fb829/fakeredis-2.37.1.tar.gz", hash = "sha256:9045851b0a9fe56312696aadc82435141aa43a193cab462d372c8fb583a7c087", size = 238182, upload-time = "2026-08-18T15:22:01.676Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/e0/fe1c760abd9e9cb5dfbce057b6582cb1ac36dfd8b5ce202e1445605c922d/fakeredis-2.37.1-py3-none-any.whl", hash = "sha256:f15c41be151c1e9206416dece764369a4dedb6b1341df3734c3c2c000e405508", size = 152738, upload-time = "2026-08-18T15:22:00.1Z" },
+]
+
[[package]]
name = "fastapi"
-version = "0.139.0"
+version = "0.141.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
@@ -845,9 +965,18 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
+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 = "filelock"
+version = "3.32.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z" },
]
[[package]]
@@ -937,61 +1066,61 @@ wheels = [
[[package]]
name = "gitpython"
-version = "3.1.57"
+version = "3.1.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/61/3285044215fb596bf093e39ccb96ece0a1076a8ca57a61e069a6a33cdb1b/gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95", size = 231680, upload-time = "2026-08-28T11:01:13.761Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/5e/49cc172da4d0578644ba37cec5cb365b1fefc603b26edea9bcac1c7f830a/gitpython-3.1.61-py3-none-any.whl", hash = "sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c", size = 222118, upload-time = "2026-08-28T11:01:12.262Z" },
]
[[package]]
name = "greenlet"
-version = "3.5.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" },
- { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" },
- { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" },
- { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" },
- { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" },
- { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" },
- { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" },
- { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" },
- { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" },
- { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" },
- { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" },
- { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" },
- { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" },
- { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" },
- { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" },
- { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" },
- { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" },
- { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" },
- { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" },
- { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" },
- { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" },
- { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" },
- { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" },
- { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" },
- { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" },
- { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" },
- { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" },
- { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" },
- { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" },
- { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" },
- { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" },
- { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" },
- { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" },
- { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" },
- { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" },
- { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" },
- { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" },
- { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" },
+version = "3.5.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" },
+ { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" },
+ { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" },
+ { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" },
+ { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" },
+ { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" },
+ { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" },
+ { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" },
+ { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" },
+ { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" },
+ { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" },
+ { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" },
+ { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" },
+ { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" },
+ { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" },
+ { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },
+ { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
]
[[package]]
@@ -1018,15 +1147,15 @@ wheels = [
[[package]]
name = "httpcore2"
-version = "2.5.0"
+version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/47/06/5c12df521b5322fb1114a83d46911b2fbcb8855ddb3a635f11c01a214af5/httpcore2-2.5.0.tar.gz", hash = "sha256:88aa170137c17328d5ac44234f9fd10706466d5fb347f3edac4d39b91137b09d", size = 64808, upload-time = "2026-06-25T14:16:56.472Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
]
[[package]]
@@ -1046,26 +1175,45 @@ wheels = [
[[package]]
name = "httpx2"
-version = "2.5.0"
+version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "anyio" },
- { name = "httpcore2" },
+ { name = "anyio", marker = "sys_platform != 'emscripten'" },
+ { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
+ { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" },
{ name = "idna" },
- { name = "truststore" },
+ { name = "truststore", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d0/e2/b5dedc0cf35aa65de5f541ccd30d2bc1fd7f1d43c9ab09f8ed9a7342317b/httpx2-2.5.0.tar.gz", hash = "sha256:e2df9cb4611021527ff8a675b1c320b610a2ec397acc8d6fe6e91df2d9b33c29", size = 83121, upload-time = "2026-06-25T14:16:57.491Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl", hash = "sha256:3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41", size = 76652, upload-time = "2026-06-25T14:16:55.23Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
+]
+
+[[package]]
+name = "httpx2-jsfetch"
+version = "1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
]
[[package]]
name = "idna"
-version = "3.18"
+version = "3.19"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
]
[[package]]
@@ -1184,51 +1332,88 @@ wheels = [
[[package]]
name = "librt"
-version = "0.13.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" },
- { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" },
- { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" },
- { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" },
- { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" },
- { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" },
- { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" },
- { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" },
- { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" },
- { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" },
- { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" },
- { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" },
- { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" },
- { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" },
- { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" },
- { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" },
- { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" },
- { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" },
- { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" },
- { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" },
- { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" },
- { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" },
- { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" },
- { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" },
- { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" },
- { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" },
- { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" },
- { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" },
- { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" },
- { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" },
- { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" },
- { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" },
- { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" },
- { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" },
- { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" },
- { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" },
- { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" },
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" },
+ { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" },
+ { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" },
+ { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" },
+ { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" },
+ { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" },
+ { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" },
+ { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" },
+ { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" },
+ { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" },
+ { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" },
+ { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" },
+ { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" },
+ { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" },
+ { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" },
+ { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" },
+ { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" },
+ { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" },
+ { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" },
+ { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" },
+ { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" },
+ { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" },
]
[[package]]
@@ -1245,23 +1430,23 @@ wheels = [
[[package]]
name = "mako"
-version = "1.3.12"
+version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" },
+ { 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.2"
+version = "3.10.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
+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/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
+ { 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]]
@@ -1321,105 +1506,117 @@ name = "mona"
version = "0.4.0"
source = { virtual = "." }
dependencies = [
- { name = "alembic" },
{ name = "bcrypt" },
{ name = "celery" },
- { name = "dotenv" },
- { name = "fastapi" },
- { name = "prometheus-fastapi-instrumentator" },
+ { name = "httpx2" },
{ name = "psycopg2-binary" },
+ { name = "pydantic" },
{ name = "pydantic-settings" },
- { name = "pyyaml" },
{ name = "redis" },
- { name = "requests" },
- { name = "skops" },
{ name = "sqlalchemy" },
- { name = "uvicorn" },
]
[package.dev-dependencies]
ci = [
- { name = "celery" },
+ { name = "fakeredis" },
{ name = "fastapi" },
{ name = "httpx" },
- { name = "httpx2" },
{ name = "mypy" },
{ name = "prometheus-fastapi-instrumentator" },
{ name = "pydantic" },
{ name = "pytest" },
+ { name = "pytest-asyncio" },
{ name = "ruff" },
- { name = "sqlalchemy" },
]
dev = [
+ { name = "alembic" },
{ name = "celery" },
{ name = "checkov" },
+ { name = "fakeredis" },
{ name = "fastapi" },
{ name = "httpx" },
- { name = "httpx2" },
+ { name = "mypy" },
{ name = "numpy" },
+ { name = "pre-commit" },
{ name = "prometheus-fastapi-instrumentator" },
- { name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pytest" },
+ { name = "pytest-asyncio" },
{ name = "pyyaml" },
- { name = "redis" },
- { name = "requests" },
{ name = "ruff" },
{ name = "scikit-learn" },
- { name = "sqlalchemy" },
+ { name = "skops" },
{ name = "uvicorn" },
]
+docker-fastapi = [
+ { name = "alembic" },
+ { name = "fastapi" },
+ { name = "prometheus-fastapi-instrumentator" },
+ { name = "uvicorn" },
+]
+worker = [
+ { name = "numpy" },
+ { name = "pyyaml" },
+ { name = "scikit-learn" },
+ { name = "skops" },
+]
[package.metadata]
requires-dist = [
- { name = "alembic", specifier = ">=1.18.5" },
{ name = "bcrypt", specifier = ">=5.0.0" },
{ name = "celery", specifier = ">=5.6.3" },
- { name = "dotenv", specifier = ">=0.9.9" },
- { name = "fastapi", specifier = ">=0.139.0" },
- { name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" },
+ { name = "httpx2", specifier = ">=2.12.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.12" },
- { name = "pydantic-settings", specifier = ">=2.14.2" },
- { name = "pyyaml", specifier = ">=6.0.1" },
- { name = "redis", specifier = ">=8.0.1" },
- { name = "requests", specifier = ">=2.34.2" },
- { name = "skops", specifier = ">=0.14.0" },
- { name = "sqlalchemy", specifier = ">=2.0.51" },
- { name = "uvicorn", specifier = ">=0.51.0" },
+ { name = "pydantic", specifier = ">=2.13.5" },
+ { name = "pydantic-settings", specifier = ">=2.15.0" },
+ { name = "redis", specifier = ">=8.1.0" },
+ { name = "sqlalchemy", specifier = ">=2.0.52" },
]
[package.metadata.requires-dev]
ci = [
- { name = "celery", specifier = ">=5.6.3" },
+ { name = "fakeredis", specifier = ">=2.37.1" },
{ name = "fastapi", specifier = ">=0.139.0" },
{ name = "httpx", specifier = ">=0.28.1" },
- { name = "httpx2", specifier = ">=2.5.0" },
{ name = "mypy", specifier = ">=2.2.0" },
{ name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" },
{ name = "pydantic", specifier = ">=2.13.4" },
{ name = "pytest", specifier = ">=9.1.1" },
+ { name = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "ruff", specifier = ">=0.15.21" },
- { name = "sqlalchemy", specifier = ">=2.0.51" },
]
dev = [
+ { name = "alembic", specifier = ">=1.19.1" },
{ name = "celery", specifier = ">=5.6.3" },
{ name = "checkov", specifier = ">=3.3.8" },
+ { name = "fakeredis", specifier = ">=2.37.1" },
{ name = "fastapi", specifier = ">=0.139.0" },
{ name = "httpx", specifier = ">=0.28.1" },
- { name = "httpx2", specifier = ">=2.5.0" },
+ { name = "mypy", specifier = ">=2.3.1" },
{ name = "numpy", specifier = ">=2.5.1" },
+ { name = "pre-commit", specifier = ">=4.6.1" },
{ name = "prometheus-fastapi-instrumentator", specifier = ">=8.0.2" },
- { name = "psycopg2-binary", specifier = ">=2.9.12" },
{ name = "pydantic", specifier = ">=2.13.4" },
{ name = "pytest", specifier = ">=9.1.1" },
+ { name = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "pyyaml", specifier = ">=6.0.1" },
- { name = "redis", specifier = ">=8.0.1" },
- { name = "requests", specifier = ">=2.34.2" },
{ name = "ruff", specifier = ">=0.15.21" },
{ name = "scikit-learn", specifier = ">=1.9.0" },
- { name = "sqlalchemy", specifier = ">=2.0.51" },
+ { name = "skops", specifier = ">=0.14.0" },
{ name = "uvicorn", specifier = ">=0.51.0" },
]
+docker-fastapi = [
+ { name = "alembic", specifier = ">=1.19.1" },
+ { name = "fastapi", specifier = ">=0.141.1" },
+ { name = "prometheus-fastapi-instrumentator", specifier = ">=8.1.0" },
+ { name = "uvicorn", specifier = ">=0.52.4" },
+]
+worker = [
+ { name = "numpy", specifier = ">=2.5.1" },
+ { name = "pyyaml", specifier = ">=6.0.1" },
+ { name = "scikit-learn", specifier = ">=1.9.0" },
+ { name = "skops", specifier = ">=0.14.0" },
+]
[[package]]
name = "multidict"
@@ -1504,7 +1701,7 @@ wheels = [
[[package]]
name = "mypy"
-version = "2.2.0"
+version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ast-serialize" },
@@ -1513,31 +1710,41 @@ dependencies = [
{ name = "pathspec" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e9/7e/be536678c6ae49ef058aba4b483d8c7bc104f471479016066f345bc1f5f8/mypy-2.2.0.tar.gz", hash = "sha256:2cdd99d48590dce6f6b7f1961eda75386364398fcdaad86923bc0f0231bf9baf", size = 3950939, upload-time = "2026-07-08T01:37:27.335Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/5f/2d7a9ac5646274cd6e77ce3abcc2a9ece760c2b21f4c4b9f301711e07855/mypy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6968f27347ef539c443ddfd6897e79db525ddb8c856aa8fbf14c34f310ca5193", size = 14931618, upload-time = "2026-07-08T01:33:51.702Z" },
- { url = "https://files.pythonhosted.org/packages/b2/8a/1adaa7caaa104f87021b1ac71252d62e646e9b623d77900ac7a0ae252bf3/mypy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df226d2a0ae2c3b03845af217800a68e2965d4b14914c99b78d3a2c8ae23299", size = 13857718, upload-time = "2026-07-08T01:35:27.555Z" },
- { url = "https://files.pythonhosted.org/packages/1d/15/b11586b5aebbb82213e297fc30a6fcf3bed6a9deea3739cd8dd87621f3fd/mypy-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc53553996aca2094216ad9306a6f06c5265d206c1bcb54dd367560bd3557825", size = 14059704, upload-time = "2026-07-08T01:33:57.363Z" },
- { url = "https://files.pythonhosted.org/packages/03/db/071e05ab442596bdf7a845e830d5ef7128a0175281038245b171a6b16873/mypy-2.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:996abf2f0bebf572556c60720e8dc0cf5292b64060fa68d7f2bc9caa48e01b6f", size = 15128719, upload-time = "2026-07-08T01:33:33.855Z" },
- { url = "https://files.pythonhosted.org/packages/f3/1c/c4b84eafb85ee315da72471523cc1bf7d7c42164085c42333601da7a8817/mypy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ec287c2381898c652bf8ff79448627fe1a9ee76d22005181fac7315a485c7108", size = 15378692, upload-time = "2026-07-08T01:34:10.468Z" },
- { url = "https://files.pythonhosted.org/packages/68/a4/59a0ee94877fdfe2958cec9b6add72a75393063c79cb60ab4026dd5e10c2/mypy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2c967a7685fa93fcf1a778b3ebe76e756b28ba14655f539d7b61ff3da69352a", size = 11150911, upload-time = "2026-07-08T01:35:21.603Z" },
- { url = "https://files.pythonhosted.org/packages/90/e4/6a9144be50180ed43d8c92de9b03dff504daa92b5bcc0353e8960799a23b/mypy-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:d59a4b80351ec92e5f7415fcdd008bd77fcbefc7adf9cbc7ffe4eb9f71617734", size = 10125389, upload-time = "2026-07-08T01:36:36.164Z" },
- { url = "https://files.pythonhosted.org/packages/73/32/0aa8d8d197023ca6040f7b25a486cb47037b6350b0d3bae657c8f85fb43f/mypy-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1f6c3d76853071409ac58fc0aadfb276a22af5f190fdaa02152a858088a39ebd", size = 14926083, upload-time = "2026-07-08T01:36:02.365Z" },
- { url = "https://files.pythonhosted.org/packages/e2/7c/35bbe0cb10e6699f90e988e537aaf4282a6c16e37f58848a242eb0a98bde/mypy-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bdaa177e80cc3292824d4ef3670b5b58771ee8d57c290e0c9c89e7968212332c", size = 13879985, upload-time = "2026-07-08T01:36:31.093Z" },
- { url = "https://files.pythonhosted.org/packages/f9/0c/1597fbebd873e9b63452317740ae3dd32692cec5da180cc65acd96cd28cf/mypy-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80923e6d6e7878291f537ee11052f974954c20cb569798429a5dc265eb780b47", size = 14076883, upload-time = "2026-07-08T01:34:21.789Z" },
- { url = "https://files.pythonhosted.org/packages/68/35/2ec021a83ec01b5d522639f78d8b36adade7fa4821db0f48fd6d82e861f3/mypy-2.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f24bd465a09077c8d64be8f19a6646db467a55490fd315fe7871afe6bb9645", size = 15103567, upload-time = "2026-07-08T01:36:47.717Z" },
- { url = "https://files.pythonhosted.org/packages/53/3a/8cb3529f6d6800c7d069935e5c83a05d80263847b8a947cf6b0b16a9e958/mypy-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db34595464869f474708e769413d1d739fc33a69850f253757b9a4cc20bc1fec", size = 15354641, upload-time = "2026-07-08T01:36:41.592Z" },
- { url = "https://files.pythonhosted.org/packages/d6/4d/320bc9a9553f8a9db5e847ec5ded762ef7ed7403c76c4ba2e8181c80e2f0/mypy-2.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b48092132c7b0ef4322773fecae62fc5b0bc339be348badeec8af502122a4a51", size = 7694355, upload-time = "2026-07-08T01:37:03.291Z" },
- { url = "https://files.pythonhosted.org/packages/90/05/bf3b349e2f885cd3aab488111bb9049439c28bc028dac5073350d3df8fbe/mypy-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:6fc0e98b95e31755ca06d89f75fafa7820fbb3ea2caace6d83cba17625cd0acb", size = 11329146, upload-time = "2026-07-08T01:35:38.318Z" },
- { url = "https://files.pythonhosted.org/packages/41/a5/558b06e6cfe17ab88bb38f7b370b6bc68a74ba177c9e138db9748e422d2d/mypy-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc73a5b4d40e8a3e6b12ef82eb0c90430964e34016a36c2aff4e3bfe37ba41f0", size = 10316586, upload-time = "2026-07-08T01:36:25.458Z" },
- { url = "https://files.pythonhosted.org/packages/0b/21/f0b96f19a9b8ba111a45ffbe9508e818b7f6990469b38f6888943f7bfd3a/mypy-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6257bd4b4c0ae2148548b869a1ff3758e38645b92c8fe65eca401866c3c551c1", size = 15922565, upload-time = "2026-07-08T01:35:56.282Z" },
- { url = "https://files.pythonhosted.org/packages/18/f2/1dbcb20b0865d5e992541450a8c73f2fcc90f8bd7d8a4b81313e16934870/mypy-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa22b3ae862ac1ce76f5976ddd402651b5f090bcfd49c6d0484b8983a29eaf8", size = 14816515, upload-time = "2026-07-08T01:34:58.167Z" },
- { url = "https://files.pythonhosted.org/packages/84/a2/18cce9c7d5b4d14010d1f13836da11b234dda917b17ca8671fc32c136997/mypy-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a430bf26fe8cf372f3933fbd83e633d6561868803645a20e4e6d4523f52a3b", size = 15272246, upload-time = "2026-07-08T01:37:08.72Z" },
- { url = "https://files.pythonhosted.org/packages/ce/71/24d720c7924829bd675cbde2d0fa779f50abf676ca617f53d6a8bfef5fa7/mypy-2.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d58655a60e823b1a4c9ebcda072897fb0193c2f3e6f8e7c433e152aa4cb00233", size = 16226295, upload-time = "2026-07-08T01:34:51.861Z" },
- { url = "https://files.pythonhosted.org/packages/b7/5e/785730990fc863ad8340b4ab44ac4ca23270aecff92c180ccdf27f9f5869/mypy-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4452c955caf14e28bb046cbd0c3671272e6381630a8b81b0da9713558148890", size = 16493275, upload-time = "2026-07-08T01:35:09.337Z" },
- { url = "https://files.pythonhosted.org/packages/93/33/55b1edf16f639f153972380d6977b81f65509c5b8f9c86b58b94b7990b03/mypy-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:68f5b7f7f755200f68c7181e3dfb28be9858162257690e539759c9f57721e388", size = 11749038, upload-time = "2026-07-08T01:35:50.071Z" },
- { url = "https://files.pythonhosted.org/packages/61/36/67424748a4e65e97f0e05bf00df379dfb6c2d817f82cc3a4ce5c96d99beb/mypy-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:78d201accfafce3801d978f2b8dbbd473a9ce364cc0a0dfd9192fe47d977e129", size = 10704254, upload-time = "2026-07-08T01:37:17.971Z" },
- { url = "https://files.pythonhosted.org/packages/28/cb/142c2097ca02c0d295b00625ff946808bdda65acda17d163c680d8a6a474/mypy-2.2.0-py3-none-any.whl", hash = "sha256:ecc138da861e932d1344214da4bae866b21900a9c2778824b51fe2fb47f5180e", size = 2726094, upload-time = "2026-07-08T01:34:00.075Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" },
+ { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" },
+ { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
+ { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
+ { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
+ { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
+ { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
+ { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
+ { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
+ { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
+ { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
]
[[package]]
@@ -1551,11 +1758,11 @@ wheels = [
[[package]]
name = "narwhals"
-version = "2.23.0"
+version = "2.25.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/66ed1fc6e38a0c0f330627ec5c5d597990d6159b6712b82af0ad2c65f06c/narwhals-2.23.0.tar.gz", hash = "sha256:13e7ff5b4bb4a2f77b907c2e4d8a76e273dfc1323a3c997440a2f9fd26aed408", size = 656209, upload-time = "2026-07-01T11:21:53.278Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/4e/afc8c31605cb8be1d3bb4438c4d979daa104dab6306cd2b87abe9c3a7299/narwhals-2.23.0-py3-none-any.whl", hash = "sha256:769e7b9ab102c93d8fa019f6b4cd1a657909b04a20bf6210e5a35aae06814ae9", size = 458938, upload-time = "2026-07-01T11:21:51.677Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" },
]
[[package]]
@@ -1567,82 +1774,116 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/93/aa6613aa70d6eb4868e667068b5a11feca9645498fd31b954b6c4bb82fa5/networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef", size = 1927288, upload-time = "2021-09-09T22:09:39.016Z" },
]
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
[[package]]
name = "numpy"
-version = "2.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" },
- { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" },
- { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" },
- { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" },
- { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" },
- { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" },
- { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" },
- { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" },
- { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" },
- { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" },
- { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" },
- { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" },
- { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" },
- { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" },
- { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" },
- { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" },
- { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" },
- { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" },
- { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" },
- { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" },
- { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" },
- { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" },
- { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" },
- { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" },
- { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" },
- { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" },
- { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" },
- { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" },
- { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" },
- { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" },
+version = "2.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" },
+ { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" },
+ { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" },
+ { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" },
+ { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" },
+ { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" },
+ { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" },
+ { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" },
+ { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" },
+ { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" },
+ { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" },
+ { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" },
+ { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" },
+ { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" },
+ { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" },
+ { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" },
]
[[package]]
name = "orjson"
-version = "3.11.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
- { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
- { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
- { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
- { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
- { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
- { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
- { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
- { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
- { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
- { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
- { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
- { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
- { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
- { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
- { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
- { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
- { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
- { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
- { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
- { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
- { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
- { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
- { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
- { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
- { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
- { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
- { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
+version = "3.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" },
+ { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" },
+ { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" },
+ { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" },
+ { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" },
+ { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" },
+ { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" },
+ { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" },
+ { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" },
]
[[package]]
@@ -1674,11 +1915,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.11.0"
+version = "4.11.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z" },
]
[[package]]
@@ -1716,6 +1957,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/fa/abd81b7ce825250e3e7e02f2a94315707db28a4426c8c35ca5bde92a75cf/policy_sentry-0.13.2-py3-none-any.whl", hash = "sha256:e82c3bc1783606449399c4221f67d05f6b08d8a184ba2fee87d04541d7282b86", size = 966631, upload-time = "2024-12-01T11:26:42.603Z" },
]
+[[package]]
+name = "pre-commit"
+version = "4.6.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" },
+]
+
[[package]]
name = "prettytable"
version = "3.18.0"
@@ -1730,36 +1987,36 @@ wheels = [
[[package]]
name = "prometheus-client"
-version = "0.25.0"
+version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" },
]
[[package]]
name = "prometheus-fastapi-instrumentator"
-version = "8.0.2"
+version = "8.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "prometheus-client" },
{ name = "starlette" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1b/e9/2065686d1dfa62296fdc158b6e8fd25b0cb3dca09b0632cabeb5ae81fe4d/prometheus_fastapi_instrumentator-8.0.2.tar.gz", hash = "sha256:3c252e748151768a7aefd66824a04a870144f71de48a67aed211749a9ca2a548", size = 21342, upload-time = "2026-06-23T09:39:31.611Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/f4/cdcebf7094b03b99fba71ac8f56bd6f227973642662f49d272332d8419b3/prometheus_fastapi_instrumentator-8.1.0.tar.gz", hash = "sha256:b77f3043665e8d28e2bbd21017506195a43d9adf1d402d01bf95b494b7e560e1", size = 20492, upload-time = "2026-07-26T11:12:44.202Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/c7/fa2b3f469a2e6001b829e0d6bc8680755349aa1329a87bf48731e9d5d30a/prometheus_fastapi_instrumentator-8.0.2-py3-none-any.whl", hash = "sha256:746002ec1e2c58b93f61444e1d104de959a9463a6a3f1c8909ac3757e16c3866", size = 20549, upload-time = "2026-06-23T09:39:32.616Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b9/91a2246e6cf01b7ccb14479803c8a50f9c258ae5c6a0f16ba3294820632b/prometheus_fastapi_instrumentator-8.1.0-py3-none-any.whl", hash = "sha256:b9f40b2cff3f7891ca0610b3ae4fc6ec723fd326b04bb659819aaeb821a0fc7d", size = 19649, upload-time = "2026-07-26T11:12:45.168Z" },
]
[[package]]
name = "prompt-toolkit"
-version = "3.0.52"
+version = "3.0.53"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
]
[[package]]
@@ -1956,7 +2213,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.13.4"
+version = "2.13.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -1964,88 +2221,88 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
]
[[package]]
name = "pydantic-core"
-version = "2.46.4"
+version = "2.46.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
- { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
- { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
- { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
- { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
- { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
- { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
- { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
- { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
- { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
- { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
- { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
- { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
- { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
- { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
- { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
- { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
- { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
- { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
- { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
- { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
- { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
- { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
- { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
- { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
- { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
- { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
- { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
- { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
- { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
- { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
- { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
- { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
- { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
- { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
- { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
- { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
- { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
- { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
- { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
+ { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
+ { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
+ { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
+ { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
+ { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
+ { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
+ { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
+ { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
+ { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
+ { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
+ { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
]
[[package]]
name = "pydantic-settings"
-version = "2.14.2"
+version = "2.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
]
[[package]]
name = "pygments"
-version = "2.20.0"
+version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
@@ -2073,6 +2330,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -2085,13 +2354,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
+[[package]]
+name = "python-discovery"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/96/0f93e27c9f60a650838f2118159aa115fd5732c0716247917b7ba7ede665/python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c", size = 82849, upload-time = "2026-08-28T17:30:02.366Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/5e/21abf578182fb15006a57faf3711a1e659e29d600d19b6e557eae908c81d/python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a", size = 38451, upload-time = "2026-08-28T17:30:01.236Z" },
+]
+
[[package]]
name = "python-dotenv"
-version = "1.2.2"
+version = "1.2.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
]
[[package]]
@@ -2160,11 +2441,11 @@ wheels = [
[[package]]
name = "redis"
-version = "8.0.1"
+version = "8.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/c3/928b290c2c0ca99ab96eea5b4ff8f30be8112b075301a7d3ba214a3c8c12/redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0", size = 5114170, upload-time = "2026-06-23T14:52:37.728Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/99/604f0b666d4c616d891cf77ebb9db6bb21601344c051aebf1b72b9ff915f/redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25", size = 5254356, upload-time = "2026-07-30T08:51:00.269Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743", size = 502406, upload-time = "2026-06-23T14:52:36.137Z" },
+ { url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" },
]
[[package]]
@@ -2350,54 +2631,56 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.21"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
- { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
- { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
- { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
- { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
- { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
- { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
- { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
- { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
- { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
- { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
- { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
- { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
- { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
- { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
- { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
+version = "0.16.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" },
+ { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" },
+ { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" },
+ { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" },
]
[[package]]
name = "rustworkx"
-version = "0.18.0"
+version = "0.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/1a/545aa3a3e251e9da00e4acf0e5472b7fbbb15360f56d9d5342c1f93b8b93/rustworkx-0.18.0.tar.gz", hash = "sha256:5ca9cf8dbee50f8def012119ebb64771ae86916993417415aba9e845831cb436", size = 894567, upload-time = "2026-06-18T04:38:56.141Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/70/18b310752b0652d4a755b46853268c00c33401101a47f87697ad25966453/rustworkx-0.18.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7440ba70b87bd16811d92e57d76108840dbdd89aa2fc55e4d324fa2fb6b7f9c9", size = 2295936, upload-time = "2026-06-18T04:38:01.598Z" },
- { url = "https://files.pythonhosted.org/packages/02/4e/09152f4422204f020346a8a2b0e2f05f12d8cc60eeeee99b24f649985514/rustworkx-0.18.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7fce5218ebfb8d8f5313def1f15aad8d3c3c2bbe03e33805dff8cf8bb70370a1", size = 2145057, upload-time = "2026-06-18T04:38:03.253Z" },
- { url = "https://files.pythonhosted.org/packages/22/68/69198f41f7f9c39fb5b2e561bf41d4cbd117fa5dfdf4631d8ba28e5ff69b/rustworkx-0.18.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7e0c626f76bc71d414a02502be7ec0ac2dd6eca369886bc66a2650607f2d9de6", size = 2391659, upload-time = "2026-06-18T04:38:04.942Z" },
- { url = "https://files.pythonhosted.org/packages/e1/81/8ac568efe0289b0ecd826cd697c3581b6a25e134145926cbfef762656167/rustworkx-0.18.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:91b37c5bb54e233e16c4f47383385f17be03e6a344821c2f6a39a0aebee54538", size = 2189469, upload-time = "2026-06-18T04:38:06.51Z" },
- { url = "https://files.pythonhosted.org/packages/f2/a4/96492d7af2ddcd15368f80329e3514060b122f306c4406fac98c08a97865/rustworkx-0.18.0-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:bcd15f7a637ca0654f329ed42a6db0cf7eac15ed89e0b32cb9d16b1b21937979", size = 2504116, upload-time = "2026-06-18T04:38:43.441Z" },
- { url = "https://files.pythonhosted.org/packages/4d/b0/005383bd7dc110b06ca731bd2115482f3a8c6389e0c8fe1f7f259380c4c8/rustworkx-0.18.0-cp310-abi3-manylinux_2_28_s390x.whl", hash = "sha256:567ace3b0d8ac709dc4ad4ce164472f5f59508739355221480170327a6736777", size = 3170967, upload-time = "2026-06-18T04:38:39.929Z" },
- { url = "https://files.pythonhosted.org/packages/0d/2f/95ca9ef9285cc6793b3e041efca6c5a194ca6731b0af732631ee074a67b3/rustworkx-0.18.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b91cdcffeab5f498de44905a373e47e345da8f6c4f40fc969421548c4e7a5219", size = 2254008, upload-time = "2026-06-18T04:38:08.245Z" },
- { url = "https://files.pythonhosted.org/packages/19/7b/9df6a80162e6f90fd9945e3f8063c2aa41285c27aefce1d171f99c2b829f/rustworkx-0.18.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03dd1ab42974bc0ff957eccc330c0c0d9887d88240c0cd050039e215af5a3c45", size = 2447140, upload-time = "2026-06-18T04:38:09.767Z" },
- { url = "https://files.pythonhosted.org/packages/5d/b3/4d159b33bbc73dbebad050a0226dcfb0753d7f05fa55a8d5351e85c8c953/rustworkx-0.18.0-cp310-abi3-win32.whl", hash = "sha256:eb87a45864ffe36e4d86e826d12bb54dac9e4f76c214997dee624281d8711684", size = 2057237, upload-time = "2026-06-18T04:38:11.28Z" },
- { url = "https://files.pythonhosted.org/packages/99/1b/b0dc8d3751c0c58d16db72bff2954a9d3871c166147007590a3aaec4d42e/rustworkx-0.18.0-cp310-abi3-win_amd64.whl", hash = "sha256:602df896b4479b83c6456f702f8ba2ac1cbb972b30723d5fe2e84e6ff3de7d70", size = 2289224, upload-time = "2026-06-18T04:38:12.961Z" },
- { url = "https://files.pythonhosted.org/packages/f4/eb/147e86d56de0511ee7526e5cdf6144013d9c71ba4d6ed6dcd82b6b545861/rustworkx-0.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bba66b268e94efd9181d49241b2547d783770dcea9ee8c64609306a7b20ace74", size = 2339568, upload-time = "2026-06-18T04:38:14.292Z" },
- { url = "https://files.pythonhosted.org/packages/7d/1b/c665d30c132e73d69ca95cf552179608de6f94434ee5eb321297ef393920/rustworkx-0.18.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d51ff00fa72144813c2af33c673b7faf7182ca0ffc85cf35569b5ac4de84bc3e", size = 2118746, upload-time = "2026-06-18T04:38:15.759Z" },
- { url = "https://files.pythonhosted.org/packages/c5/78/04733471aa616dc0bc76f2c2e65ad89bb4ecbbfd2309231347c293906181/rustworkx-0.18.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7099bf90fc1a7ad1789bc126e3e208d9944af2dd9f9b28f75ae148c68aa00cb", size = 2372040, upload-time = "2026-06-18T04:38:17.082Z" },
- { url = "https://files.pythonhosted.org/packages/bd/eb/58ed5f78ca608156ac2ce3166e41976c32345bbc36e71a3fc18390cc4de2/rustworkx-0.18.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f562d5eeb2943024c85f631df713e7873ea8b572ada9044be82f29f923c1463a", size = 2178071, upload-time = "2026-06-18T04:38:18.571Z" },
- { url = "https://files.pythonhosted.org/packages/bf/96/888c7849d20b7e49d978cfe9bdd8873d709e220a09f8f6bdce226ef7c2be/rustworkx-0.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8437bca6adff8e91089bd5d176ef8a499085135031e2d2df4132821578e94dd6", size = 2244227, upload-time = "2026-06-18T04:38:19.923Z" },
- { url = "https://files.pythonhosted.org/packages/99/47/621b56c3f10138a02da9dfd636780ec9311c821fff1acf43be7292408625/rustworkx-0.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba9e9f91d0d14d21666f0d7549f4d8ed70dc84bf75483c68eee5a005ed900f3e", size = 2426773, upload-time = "2026-06-18T04:38:21.233Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/86/0d/5b6b48005bbc19622ea7f295f2d5f1339474cb7893e57fd30e6553b2b534/rustworkx-0.18.1.tar.gz", hash = "sha256:30affe6ee52a6257a01152418f9c1686ca114e7ab4a3bf87bb87fe35b7350f3e", size = 896598, upload-time = "2026-07-30T00:19:08.074Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/1d/df08af787e15d10479e9c69278933904e5dfa716e9793bce6e48c4ef8fb1/rustworkx-0.18.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c622843757ce6afd950b6f3c45b79f0078fb8d9a66f6783b5963594d1f9073bf", size = 2261725, upload-time = "2026-07-30T00:17:57.184Z" },
+ { url = "https://files.pythonhosted.org/packages/54/e6/08c5d1e21d3f97f172210f409fa6de12684de1b36952ec7c1f4545cfb5af/rustworkx-0.18.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:77103c119e71d816c04bcc60443184cae9550c1d49e5c28a046850252c0e9846", size = 2110294, upload-time = "2026-07-30T00:17:59.109Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/ec/80ffcc38ffd28798a31563485a7c35d7d7ccebad83d388717442731e7477/rustworkx-0.18.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ebd2441a51c68a784df8c62dc2e6f1a9f58c0eb1c2e21fd59776e08f55c0c0b", size = 2366881, upload-time = "2026-07-30T00:18:00.777Z" },
+ { url = "https://files.pythonhosted.org/packages/86/6c/e1483aea43fd8be81666cb103aea0f6127f71731de666c156e6b6f8c2338/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c42a0a52463ff78c0dd03426efa7b40b17b433689c2a5ab6aaaf14173c2a9e31", size = 2157792, upload-time = "2026-07-30T00:18:02.734Z" },
+ { url = "https://files.pythonhosted.org/packages/04/46/b6df0cddd2e22ce7b0baf24c0e5a123df6dd29d007df601ebe7b25928088/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:ba997e8c22564bf17b110514fba530bfd0ee2868506c3e8e4b4bd3f388e87b3d", size = 2476541, upload-time = "2026-07-30T00:19:09.932Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ad/6e53d1db486697bb971a2d0623b1a67903825fb9e5a44ec993f04b8678c5/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_s390x.whl", hash = "sha256:48d42983e62412e3ffc1e58c2ec55222ec82fc0e4c6dac576ee203d71091038e", size = 3102596, upload-time = "2026-07-30T00:19:07.669Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/80/f83b4469f4c2756c06741f7fd28209821827bee9e55217acdea63bbd71df/rustworkx-0.18.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5ebcdd8c55d91583c94aa62ef332de3a724b69a57ae2b5c9fcf7051f3b41fc88", size = 2223596, upload-time = "2026-07-30T00:18:04.558Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c3/b24190513dee8bf5e26ac9ef2303e8cc0a27f33581dc7f0321a2ed5eb63e/rustworkx-0.18.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:212d0a4b5ccec8cc8c3879dd1fd13b1e61894c708185f12eb63ae571349c4d95", size = 2413536, upload-time = "2026-07-30T00:18:06.2Z" },
+ { url = "https://files.pythonhosted.org/packages/52/33/e8468d7dfb059aaaef3294099e1ff945c0f7bf8b7b11ad6d807ee0a171df/rustworkx-0.18.1-cp310-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67dcdfcc3dfc8262f7627d2b3ae85ac74de1b1dcababf637a1116444c4adb621", size = 1328608, upload-time = "2026-07-30T00:19:05.143Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/00/d75ef40fc92267571b4547b0d04d8cb8c1583cfa2e437ee2d25df3dc8aff/rustworkx-0.18.1-cp310-abi3-win32.whl", hash = "sha256:f8ad39453d65c85111ba887377899d568ded6ecfb5384f3182483a23426b8f58", size = 2053170, upload-time = "2026-07-30T00:18:07.837Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/fc/c0961292208d5dda29eb8149ee395ac92fbfdff2649c5d8d2dd68f7a11a5/rustworkx-0.18.1-cp310-abi3-win_amd64.whl", hash = "sha256:91feb30971df6ac53d51503970e4aac67e4d9bc7834535f2b7cd3674645003ac", size = 2283143, upload-time = "2026-07-30T00:18:09.38Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/9f/99731c2932f7a8942e414630e4a24c1bda2242abc31b3c0df287a97e0a73/rustworkx-0.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:685d4f69da6ecfd35110f4d7933c6fa392fb25c98f369b24032758d1c57d8055", size = 2308961, upload-time = "2026-07-30T00:18:11.634Z" },
+ { url = "https://files.pythonhosted.org/packages/55/59/4977b72b142673f24bb97a30370ad0b8b806f2a4341587284b852ae205b7/rustworkx-0.18.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:63784612ccefb866ab1e07c48060a3f7ff5309629a263db6ba4a0d9a84581852", size = 2085289, upload-time = "2026-07-30T00:18:13.251Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/fd/32be334e665fa9e52a5ad89ad485e925dbd2b43ee60caa79322eff16ca37/rustworkx-0.18.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c771009dee311e207772ee17277887c3bd2fbd0de6a208e8019962ca4432008", size = 2344756, upload-time = "2026-07-30T00:18:15.232Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/a911ca9918d0c47751bad7543df6881dbc697619641a1eed657ac70bbf7f/rustworkx-0.18.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ebd6f2e28940eb983f254f7fe92b474651ac149e1bedfdaa4272dff22551cf4a", size = 2145576, upload-time = "2026-07-30T00:18:16.871Z" },
+ { url = "https://files.pythonhosted.org/packages/82/ed/e6dd81cb4e26c14824dba74629537f36b77e3ae6303775897e6e27a7fbf6/rustworkx-0.18.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:80a397a164003d9ff91c11357f9c34120c39bc33a62d095dcef2b75dd4feec51", size = 2448424, upload-time = "2026-07-30T00:19:11.853Z" },
+ { url = "https://files.pythonhosted.org/packages/64/00/ebf78ee0fadb982aec66f8f85801725228a12a15c39577b39cc1ed45c221/rustworkx-0.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b23264fcd2feb254ce5a1bea641cad9f97e06202c5c0904d5fc6abd95c6113ef", size = 2209102, upload-time = "2026-07-30T00:18:19.128Z" },
+ { url = "https://files.pythonhosted.org/packages/37/0b/05bd31d19d0d74658f30c5bd584906c741a2e4d1fb8f5c08a0168c042553/rustworkx-0.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedc1b30330927810588b3dcf5e3032c5379cd19580fd01ec8e13f1cec55c30d", size = 2392538, upload-time = "2026-07-30T00:18:21.141Z" },
]
[[package]]
@@ -2459,43 +2742,63 @@ wheels = [
[[package]]
name = "scipy"
-version = "1.18.0"
+version = "1.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" },
- { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" },
- { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" },
- { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" },
- { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" },
- { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" },
- { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" },
- { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" },
- { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" },
- { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" },
- { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" },
- { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" },
- { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" },
- { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" },
- { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" },
- { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" },
- { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" },
- { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" },
- { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" },
- { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" },
- { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" },
- { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" },
- { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" },
- { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" },
- { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" },
- { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" },
- { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" },
- { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" },
- { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" },
- { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" },
+ { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" },
+ { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" },
+ { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" },
+ { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" },
+ { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" },
+ { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" },
+ { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" },
+ { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" },
+ { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" },
+ { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" },
+ { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" },
+ { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" },
+ { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" },
+ { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" },
]
[[package]]
@@ -2552,11 +2855,11 @@ wheels = [
[[package]]
name = "soupsieve"
-version = "2.9.1"
+version = "2.9.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" },
]
[[package]]
@@ -2581,48 +2884,42 @@ wheels = [
[[package]]
name = "sqlalchemy"
-version = "2.0.51"
+version = "2.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
- { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
- { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
- { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
- { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
- { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
- { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
- { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
- { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
- { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
- { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
- { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
- { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
- { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
- { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
- { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
- { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
- { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
- { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
- { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" },
+ { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" },
+ { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" },
+ { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" },
+ { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" },
+ { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" },
]
[[package]]
name = "starlette"
-version = "1.3.1"
+version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
]
[[package]]
@@ -2684,14 +2981,14 @@ wheels = [
[[package]]
name = "typing-inspection"
-version = "0.4.2"
+version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
]
[[package]]
@@ -2744,15 +3041,15 @@ wheels = [
[[package]]
name = "uvicorn"
-version = "0.51.0"
+version = "0.52.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
]
[[package]]
@@ -2764,13 +3061,28 @@ 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 = "virtualenv"
+version = "21.7.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/41/c3f34799487924f2a6f43b8a8b7acd345a6c61aac2211d4bced8621ca4f1/virtualenv-21.7.7.tar.gz", hash = "sha256:6874376f99ba6b8d4e3ee8bde67f9285412400c7d5b29ba41ee6daa5e0221bdc", size = 5347022, upload-time = "2026-08-28T18:59:50.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/f8/dcec8dc767b812193316c7debed1e2b53e586e59c267bfe517688ff90275/virtualenv-21.7.7-py3-none-any.whl", hash = "sha256:67a6a68fef3ad8ca16b8b89f33fd8f97996cc0bf0db31629d07ecf8dec539a2c", size = 5324620, upload-time = "2026-08-28T18:59:48.056Z" },
+]
+
[[package]]
name = "wcwidth"
-version = "0.8.2"
+version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" },
]
[[package]]