From 3c9403e9390ebec4ef77eeb50253b531e9c9f87f Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Tue, 28 Jul 2026 19:22:55 +0300 Subject: [PATCH 01/35] fix readme --- README.md | 4 ++-- docker/Caddyfile | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 docker/Caddyfile diff --git a/README.md b/README.md index c50a6ad..638acc8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ MONA is a K8s-based monitoring and analytics tool managed with Terraform and Hel 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: @@ -119,7 +119,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/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 From bb632fec37b249ec1c1d942a331dcfc8e3ce0753 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Wed, 29 Jul 2026 18:10:44 +0300 Subject: [PATCH 02/35] Added api versioning --- CHANGELOG.md | 4 ++++ frontend/src/Dashboard.tsx | 16 ++++++++-------- frontend/src/DeviceAdmin.tsx | 6 +++--- frontend/src/Login.tsx | 2 +- frontend/src/ProtectedRoute.tsx | 2 +- mona_core/main.py | 8 ++++---- mona_core/security.py | 6 +++--- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb7dbd..ce4a75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG All notable changes to this project will be documented in this file. +## [v0.7.0] - 2026-07-29 - API update +### Added +- Added api versioning except probes, prometheus endpoints. + ## [v0.6.0] - 2026-07-28 - Security update ### Added - Added limiter for login diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx index 9003b59..8ed1875 100644 --- a/frontend/src/Dashboard.tsx +++ b/frontend/src/Dashboard.tsx @@ -350,7 +350,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { const fetchModelInfo = useCallback(async () => { try { - const res = await apiFetch("/model-info"); + const res = await apiFetch("/api/v1/model-info"); setModelInfo(await res.json()); } catch {} }, []); @@ -373,7 +373,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { // ── 1. Check Celery task result via /task-status ────────────────────── try { - const tr = await apiFetch(`/task-status/${taskId}`); + const tr = await apiFetch(`/api/v1/task-status/${taskId}`); if (tr.ok) { const ts = await tr.json(); // state: PENDING | STARTED | SUCCESS | FAILURE | RETRY | REVOKED @@ -388,7 +388,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { // Real success — refresh model-info stop(); await fetchModelInfo(); - const infoRes = await apiFetch("/model-info"); + const infoRes = await apiFetch("/api/v1/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; @@ -410,7 +410,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { // ── 2. Fallback: if task-status endpoint missing, detect via model-info ─ try { - const mr = await apiFetch("/model-info"); + const mr = await apiFetch("/api/v1/model-info"); if (mr.ok) { const info: ModelInfo = await mr.json(); if (info.status === "ok" && (info.model?.points_count ?? 0) !== prevPoints) { @@ -436,7 +436,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { setPhase({ kind: "submitting" }); try { const params = new URLSearchParams({ hours: trainHours, note: trainNote }); - const res = await apiFetch(`/train?${params}`, { + const res = await apiFetch(`/api/v1/train?${params}`, { method: "POST", }); const d = await res.json(); @@ -460,7 +460,7 @@ function ModelPanel({ onTrained }: { onTrained: () => void }) { if (!confirm("Reset model and return to auto-mode?")) return; setDeleting(true); try { - await apiFetch("/model", { + await apiFetch("/api/v1/model", { method: "DELETE", }); await fetchModelInfo(); @@ -636,7 +636,7 @@ export default function Dashboard() { // ─── Fetch devices list ─────────────────────────────────────────────────── const fetchDevices = useCallback(async () => { try { - const res = await apiFetch("/devices"); + const res = await apiFetch("/api/v1/devices"); const list: Device[] = await res.json(); setDevices(list); @@ -669,7 +669,7 @@ export default function Dashboard() { const fetchDashboard = useCallback(async () => { try { const params = new URLSearchParams({ hours: String(hours) }); - const res = await apiFetch(`/api/dashboard?${params}`); + const res = await apiFetch(`/api/v1/dashboard?${params}`); const d: DashboardData = await res.json(); setData(d); } catch {} diff --git a/frontend/src/DeviceAdmin.tsx b/frontend/src/DeviceAdmin.tsx index 0a6deb6..2dd3436 100644 --- a/frontend/src/DeviceAdmin.tsx +++ b/frontend/src/DeviceAdmin.tsx @@ -138,7 +138,7 @@ function AddDeviceModal({ onClose, onAdded }: AddDeviceModalProps) { setLoading(true); setError(""); try { - const res = await apiFetch("/devices", { + const res = await apiFetch("/api/v1/devices", { method: "POST", body: JSON.stringify(form), }); @@ -409,7 +409,7 @@ export default function DeviceAdmin() { const fetchDevices = useCallback(async () => { try { - const res = await apiFetch("/devices"); + const res = await apiFetch("/api/v1/devices"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setDevices(data); @@ -425,7 +425,7 @@ export default function DeviceAdmin() { const handleDeleteConfirm = async () => { if (!deviceToDelete) return; try { - const res = await apiFetch(`/devices/${deviceToDelete.id}`, { + const res = await apiFetch(`/api/v1/devices/${deviceToDelete.id}`, { method: "DELETE", }); if (!res.ok) throw new Error("Failed to delete device"); diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index c3dedf6..0c78615 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -21,7 +21,7 @@ export default function LoginPage() { setError(""); try { - const res = await fetch(`${API_BASE}/api/auth/login`, { + const res = await fetch(`${API_BASE}/api/v1/auth/login`, { method: "POST", credentials: "include", headers: { diff --git a/frontend/src/ProtectedRoute.tsx b/frontend/src/ProtectedRoute.tsx index f4084b8..0bc8a47 100644 --- a/frontend/src/ProtectedRoute.tsx +++ b/frontend/src/ProtectedRoute.tsx @@ -8,7 +8,7 @@ export default function ProtectedRoute({children}:{children:React.ReactNode}){ const location = useLocation(); useEffect(() => { - apiFetch("/api/auth/me") + apiFetch("/api/v1/auth/me") .then(res => setOk(res.ok)) .catch(() => setOk(false)) .finally(() => setLoading(false)); diff --git a/mona_core/main.py b/mona_core/main.py index 0f5e965..dce1464 100644 --- a/mona_core/main.py +++ b/mona_core/main.py @@ -267,7 +267,7 @@ def delete_model(db: Session = Depends(get_db)): # ─── Dashboard ────────────────────────────────────────────────────────────── -@user_router.get("/api/dashboard") +@user_router.get("/dashboard") def get_dashboard_data( hours: int = 1, device: str | None = None, @@ -387,6 +387,6 @@ def get_task_status(task_id: str): } -app.include_router(admin_router) -app.include_router(user_router) -app.include_router(auth_router) +app.include_router(admin_router, prefix="/api/v1") +app.include_router(user_router, prefix="/api/v1") +app.include_router(auth_router, prefix="/api/v1") diff --git a/mona_core/security.py b/mona_core/security.py index b226484..35db4bf 100644 --- a/mona_core/security.py +++ b/mona_core/security.py @@ -129,7 +129,7 @@ async def require_admin(user: dict = Depends(get_current_user)) -> dict: # ─── login & logout ───────────────────────────────────────────────────────── -@auth_router.post("/api/auth/login", tags=["Auth"]) +@auth_router.post("/auth/login", tags=["Auth"]) async def login( body: LoginRequest, request: Request, @@ -167,7 +167,7 @@ async def login( return {"status": "ok", "message": "Successfully login", "role": user.role} -@auth_router.post("/api/auth/logout", tags=["Auth"]) +@auth_router.post("/auth/logout", tags=["Auth"]) async def logout(response: Response, user_session: str | None = Cookie(None)): if user_session: await redis_client.delete(f"session:{user_session}") @@ -176,6 +176,6 @@ async def logout(response: Response, user_session: str | None = Cookie(None)): return {"status": "ok", "message": "Successfuly logout"} -@auth_router.get("/api/auth/me") +@auth_router.get("/auth/me") async def auth_me(admin=Depends(get_current_user)): return {"authenticated": True} From de58703adb27c390f99c1e3ad1e913e0078664dc Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Wed, 29 Jul 2026 19:35:56 +0300 Subject: [PATCH 03/35] added pre commit --- .pre-commit-config.yaml | 70 +++++++++++++++++++ .yamllint.yaml | 12 ++++ README.md | 40 +++++++---- pyproject.toml | 4 ++ tests/test_api.py | 148 ++++++++++++++++++++-------------------- uv.lock | 91 ++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .yamllint.yaml 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..5b87200 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,12 @@ +extends: default + +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 \ No newline at end of file diff --git a/README.md b/README.md index 638acc8..c9852da 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ # MONA - Monitor & Analytics Tool +
+ +![CI Pipeline](https://img.shields.io/badge/CI_Pipeline-Automated_Checks-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) +![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white) +![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white) +![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=for-the-badge&logo=kubernetes&logoColor=white) +![Terraform](https://img.shields.io/badge/Terraform-844FBA?style=for-the-badge&logo=terraform&logoColor=white) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?style=for-the-badge&logo=postgresql&logoColor=white) + +
## 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. @@ -11,11 +21,13 @@ MONA is a K8s-based monitoring and analytics tool managed with Terraform and Hel 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 ``` @@ -27,8 +39,8 @@ make all * **Admin panel:** `localhost:30081/admin` *Default username: admin, password: admin123* * **Dashboards:** `localhost:30081/admin/dashboard` * **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 @@ -38,22 +50,22 @@ make all * 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` +* Anomalies[Get]: `localhost:30080/api/v1/anomalies` +* Model-info[Get]: `localhost:30080/api/v1/model-info` +* Devices[Get]: `localhost:30080/api/v1/devices` +* Devices[Post]: `localhost:30080/api/v1/devices` *available via curl* +* Devices[Delete]: `localhost:30080/api/v1/devices/{device_id}` *available via curl* +* Train[Post]: `localhost:30080/api/v1/train` *available via curl* +* Model[Delete]: `localhost:30080/api/v1/model` *available via curl* +* Dashboard[Get]: `localhost:30080/api/v1/dashboard` #### 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` +* Login[Post]: `localhost:30080/api/v1/auth/login` +* Logout[Post]: `localhost:30080/api/v1/auth/logout` +* Check auth[Get]: `localhost:30080/api/v1auth/me`
diff --git a/pyproject.toml b/pyproject.toml index 7b513c2..eabc4ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,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" @@ -74,4 +77,5 @@ dev = [ "uvicorn>=0.51.0", "scikit-learn>=1.9.0", "checkov>=3.3.8", + "pre-commit>=4.6.1", ] diff --git a/tests/test_api.py b/tests/test_api.py index 8f09630..65f9384 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -31,13 +31,13 @@ def broken_execute(self, *args, **kwargs): class TestDevices: def test_get_device_empty(self, client, mock_user_auth): - resp = client.get("/devices") + 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("/devices", json=payload) + resp = client.post("/api/v1/devices", json=payload) assert resp.status_code == 201 body = resp.json() @@ -48,24 +48,24 @@ def test_create_device(self, client, mock_admin_auth): 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) + resp = client.post("/api/v1/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) + first = client.post("/api/v1/devices", json=payload) assert first.status_code == 201 - second = client.post("/devices", json={"ip": "10.0.0.2", "name": "dup-name"}) + second = client.post("/api/v1/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"}) + 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("/devices") + resp = client.get("/api/v1/devices") assert resp.status_code == 200 names = {d["name"] for d in resp.json()} assert names == {"a", "b"} @@ -76,15 +76,15 @@ def test_delete_device_success(self, client, db_session, mock_admin_auth): db_session.commit() db_session.refresh(dev) - resp = client.delete(f"/devices/{dev.id}") + resp = client.delete(f"/api/v1/devices/{dev.id}") assert resp.status_code == 200 assert resp.json() == {"message": "Device deleted successfully"} - resp2 = client.get("/devices") + resp2 = client.get("/api/v1/devices") assert resp2.json() == [] def test_delete_device_not_found(self, client, mock_admin_auth): - resp = client.delete("/devices/99999") + resp = client.delete("/api/v1/devices/99999") assert resp.status_code == 404 assert resp.json()["detail"] == "Device not found" @@ -103,7 +103,7 @@ class TestValidation: ) 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) + resp = client.post("/api/v1/devices", json=payload) assert resp.status_code == 422 assert ( @@ -124,7 +124,7 @@ def test_create_device_invalid_ip(self, client, ip, name, mock_admin_auth): ) 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) + resp = client.post("/api/v1/devices", json=payload) assert resp.status_code == 422 assert ( @@ -142,26 +142,26 @@ def test_create_device_invalid_name(self, ip, name, client, mock_admin_auth): ], ) 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}) + resp = client.post("/api/v1/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", + "/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( - "/devices", json={"ip": "10.0.0.1/24", "name": "test", "is_active": True} + "/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( - "/devices", + "/api/v1/devices", json={ "ip": " 192.168.1.30 ", "name": " trimmed-name ", @@ -174,19 +174,19 @@ def test_create_device_trims_whitespace(self, client, mock_admin_auth): 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}) + 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( - "/devices", json={"ip": "192.168.1.41", "name": 12345, "is_active": True} + "/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( - "/devices", + "/api/v1/devices", json={"ip": "::ffff:192.168.1.1", "name": "mapped-ipv6", "is_active": True}, ) assert resp.status_code == 201 @@ -195,7 +195,7 @@ def test_create_device_ipv4_mapped_ipv6(self, client, mock_admin_auth): class TestModel: def test_model_info_no_model(self, client, mock_user_auth): - resp = client.get("/model-info") + resp = client.get("/api/v1/model-info") assert resp.status_code == 200 assert resp.json() == { "status": "no_model", @@ -215,7 +215,7 @@ def test_model_info_with_model(self, client, db_session, mock_user_auth): ) db_session.commit() - resp = client.get("/model-info") + resp = client.get("/api/v1/model-info") assert resp.status_code == 200 body = resp.json() assert body["status"] == "ok" @@ -237,7 +237,7 @@ def test_model_info_ignores_non_user_trained( ) db_session.commit() - resp = client.get("/model-info") + resp = client.get("/api/v1/model-info") assert resp.json()["status"] == "no_model" def test_delete_model(self, client, db_session, mock_admin_auth): @@ -253,23 +253,23 @@ def test_delete_model(self, client, db_session, mock_admin_auth): ) db_session.commit() - resp = client.delete("/model") + 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("/model-info").json()["status"] == "no_model" + 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("/model") + resp = client.delete("/api/v1/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") + resp = client.get("/api/v1/db-metrics") assert resp.status_code == 200 assert resp.json() == {"items": [], "next_cursor": None} @@ -282,7 +282,7 @@ def test_db_metrics_filter_by_device(self, client, db_session, mock_user_auth): ) db_session.commit() - resp = client.get("/db-metrics", params={"device": "srv-1"}) + 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" @@ -307,7 +307,7 @@ def test_dashboard_combines_metrics_anomalies_and_model( ) db_session.commit() - resp = client.get("/api/dashboard", params={"hours": 1}) + resp = client.get("/api/v1/dashboard", params={"hours": 1}) assert resp.status_code == 200 body = resp.json() @@ -321,13 +321,13 @@ def test_dashboard_respects_time_window(self, client, db_session, mock_user_auth db_session.add(Metric(cpu=1, ram=1, device="srv-1", timestamp=old)) db_session.commit() - resp = client.get("/api/dashboard", params={"hours": 1}) + resp = client.get("/api/v1/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"}) + resp = client.post("/api/v1/train", params={"hours": 2.5, "note": "manual run"}) assert resp.status_code == 202 body = resp.json() @@ -341,7 +341,7 @@ def test_train_model_submits_task(self, client, mock_celery, mock_admin_auth): 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") + resp = client.post("/api/v1/train") assert resp.status_code == 202 # call = mock_celery["send_task_calls"][0] # assert call["kwargs"] == {"hours": 1.0, "note": ""} @@ -350,7 +350,7 @@ def test_train_model_defaults(self, client, mock_celery, mock_admin_auth): 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") + 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" @@ -365,7 +365,7 @@ def test_task_status_success(self, client, mock_celery, mock_user_auth): state="SUCCESS", result={"accuracy": 0.97} ) - resp = client.get("/task-status/finished-task") + resp = client.get("/api/v1/task-status/finished-task") assert resp.status_code == 200 body = resp.json() assert body["state"] == "SUCCESS" @@ -377,7 +377,7 @@ def test_task_status_failure(self, client, mock_celery, mock_user_auth): state="FAILURE", result="boom: division by zero" ) - resp = client.get("/task-status/broken-task") + resp = client.get("/api/v1/task-status/broken-task") assert resp.status_code == 200 body = resp.json() assert body["state"] == "FAILURE" @@ -406,7 +406,7 @@ def test_get_anomalies_default_window(self, client, db_session, mock_user_auth): ) db_session.commit() - resp = client.get("/anomalies") + resp = client.get("/api/v1/anomalies") assert resp.status_code == 200 body = resp.json() assert len(body) == 3 @@ -422,7 +422,7 @@ def test_get_anomalies_filter_by_device(self, client, db_session, mock_user_auth ) db_session.commit() - resp = client.get("/anomalies", params={"device": "srv-2"}) + resp = client.get("/api/v1/anomalies", params={"device": "srv-2"}) assert resp.status_code == 200 body = resp.json() assert len(body) == 3 @@ -435,7 +435,7 @@ def test_get_anomalies_hours_zero_disables_time_filter( db_session.add(self._make_anomaly("srv-1", now - timedelta(days=30))) db_session.commit() - resp = client.get("/anomalies", params={"hours": 0}) + resp = client.get("/api/v1/anomalies", params={"hours": 0}) assert resp.status_code == 200 assert len(resp.json()) == 3 @@ -449,7 +449,7 @@ def test_get_anomalies_ordered_desc(self, client, db_session, mock_user_auth): ) db_session.commit() - resp = client.get("/anomalies") + resp = client.get("/api/v1/anomalies") body = resp.json() timestamps = [a["timestamp"] for a in body["items"]] assert timestamps == sorted(timestamps, reverse=True) @@ -488,7 +488,7 @@ def test_valid_login(self, client, db_session, mock_redis): db_session.commit() resp = client.post( - "api/auth/login", json={"username": "admin", "password": "123456"} + "/api/v1/auth/login", json={"username": "admin", "password": "123456"} ) assert resp.status_code == 200 assert resp.json()["status"] == "ok" @@ -503,13 +503,13 @@ def test_invalid_login(self, client, db_session, mock_redis): db_session.commit() resp = client.post( - "api/auth/login", json={"username": "admin", "password": "123456"} + "/api/v1/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") + resp = client.get("/api/v1/auth/me") assert resp.status_code == 200 assert resp.json() == {"authenticated": True} @@ -518,7 +518,7 @@ def test_valid_logout(self, client, mock_redis): client.cookies.set("admin_session", "session123") resp = client.post( - "/api/auth/logout", + "/api/v1/auth/logout", ) assert resp.status_code == 200 @@ -545,10 +545,10 @@ def test_password_hash_random_salt(self): @pytest.mark.parametrize( "method, endpoint", [ - ("POST", "/devices"), - ("DELETE", "/devices/1"), - ("POST", "/train?hours=1"), - ("DELETE", "/model"), + ("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): @@ -559,12 +559,12 @@ def test_admin_previliges_with_user(self, client, method, endpoint, mock_user_au @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"), + ("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( @@ -577,12 +577,12 @@ def test_user_previliges_with_user( @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"), + ("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( @@ -598,21 +598,21 @@ class TestSecureEndpoints: "method, endpoint", [ # Auth - ("GET", "/api/auth/me"), + ("GET", "/api/v1/auth/me"), # Devices - ("GET", "/devices"), - ("POST", "/devices"), - ("DELETE", "/devices/1"), + ("GET", "/api/v1/devices"), + ("POST", "/api/v1/devices"), + ("DELETE", "/api/v1/devices/1"), # Model & Anomalies - ("GET", "/anomalies"), - ("GET", "/model-info"), - ("POST", "/train?hours=1"), - ("DELETE", "/model"), + ("GET", "/api/v1/anomalies"), + ("GET", "/api/v1/model-info"), + ("POST", "/api/v1/train?hours=1"), + ("DELETE", "/api/v1/model"), # Dashboard & Metrics - ("GET", "/api/dashboard"), - ("GET", "/db-metrics"), + ("GET", "/api/v1/dashboard"), + ("GET", "/api/v1/db-metrics"), # Tasks - ("GET", "/task-status/dummy-task-id-123"), + ("GET", "/api/v1/task-status/dummy-task-id-123"), ], ) def test_endpoints_without_cookie_return_401(self, client, method, endpoint): @@ -631,7 +631,7 @@ def test_login_rate_limit_exceeded(self, client, db_session, mock_redis): for _ in range(10): resp = client.post( - "/api/auth/login", + "/api/v1/auth/login", json={"username": "admin", "password": "wrong_password"} ) responses.append(resp) @@ -649,14 +649,14 @@ def test_successful_login_resets_rate_limit(self, client, db_session, mock_redis for _ in range(2): client.post( - "/api/auth/login", + "/api/v1/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", + "/api/v1/auth/login", json={"username": "admin", "password": "123456"} ) assert resp.status_code == 200 @@ -667,7 +667,7 @@ 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", + "/api/v1/auth/login", json={"username": "target_user", "password": "some_password"} ) diff --git a/uv.lock b/uv.lock index 3255191..02bd12b 100644 --- a/uv.lock +++ b/uv.lock @@ -534,6 +534,15 @@ wheels = [ { 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" }, ] +[[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" @@ -779,6 +788,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" @@ -850,6 +868,15 @@ 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" }, ] +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1059,6 +1086,15 @@ 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" }, ] +[[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" @@ -1357,6 +1393,7 @@ dev = [ { name = "httpx" }, { name = "httpx2" }, { name = "numpy" }, + { name = "pre-commit" }, { name = "prometheus-fastapi-instrumentator" }, { name = "psycopg2-binary" }, { name = "pydantic" }, @@ -1408,6 +1445,7 @@ dev = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx2", specifier = ">=2.5.0" }, { 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" }, @@ -1567,6 +1605,15 @@ 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" @@ -1716,6 +1763,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.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, +] + [[package]] name = "prettytable" version = "3.18.0" @@ -2085,6 +2148,19 @@ 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.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -2764,6 +2840,21 @@ 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.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/25/e367a7229b0914772ca8d81b41fde012d9feda68523b52644a571bb21ce8/virtualenv-21.7.0.tar.gz", hash = "sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c", size = 5527510, upload-time = "2026-07-21T13:12:14.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/7a/ae29312b1e88a22e81f5d21fc11526d2a114089776c2550d2b205b6c2a47/virtualenv-21.7.0-py3-none-any.whl", hash = "sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd", size = 5507078, upload-time = "2026-07-21T13:12:12.136Z" }, +] + [[package]] name = "wcwidth" version = "0.8.2" From f0ad88ac374e61e4a37b30d33411ef66db4960fe Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Wed, 29 Jul 2026 20:47:08 +0300 Subject: [PATCH 04/35] added contributing.md --- CONTRIBUTING.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..920253e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# 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/ +``` + + +## 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 From f2702cc179a9c0a899cd441a7eb0d7fdca892f3c Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Thu, 27 Aug 2026 19:42:02 +0300 Subject: [PATCH 05/35] enhanced frontend and added new page --- frontend/package-lock.json | 332 ++++++ frontend/package.json | 1 + frontend/src/Dashboard.tsx | 979 ------------------ frontend/src/DeviceAdmin.tsx | 594 ----------- frontend/src/Login.tsx | 99 +- frontend/src/ProtectedRoute.tsx | 4 +- frontend/src/api.tsx | 51 +- frontend/src/components/anomalyFeed.tsx | 67 ++ frontend/src/components/deviceCard.tsx | 98 ++ frontend/src/components/deviceSelector.tsx | 99 ++ .../src/components/modals/addDeviceModal.tsx | 198 ++++ .../src/components/modals/addUserModal.tsx | 186 ++++ .../components/modals/confirmDeleteModal.tsx | 44 + frontend/src/components/modals/userModal.tsx | 347 +++++++ frontend/src/components/modelPanel.tsx | 299 ++++++ frontend/src/components/periodSelector.tsx | 24 + frontend/src/components/statCard.tsx | 14 + frontend/src/components/statsBar.tsx | 25 + frontend/src/components/userCard.tsx | 35 + frontend/src/config/config.ts | 2 + frontend/src/helpers/chartToolTip.tsx | 39 + frontend/src/helpers/helpers.ts | 38 + frontend/src/helpers/toolTip.tsx | 39 + frontend/src/main.tsx | 88 +- frontend/src/pages/Dashboard.tsx | 386 +++++++ frontend/src/pages/DeviceAdmin.tsx | 245 +++++ frontend/src/pages/UserBoard.tsx | 151 +++ frontend/src/types/Types.ts | 80 ++ 28 files changed, 2883 insertions(+), 1681 deletions(-) delete mode 100644 frontend/src/Dashboard.tsx delete mode 100644 frontend/src/DeviceAdmin.tsx create mode 100644 frontend/src/components/anomalyFeed.tsx create mode 100644 frontend/src/components/deviceCard.tsx create mode 100644 frontend/src/components/deviceSelector.tsx create mode 100644 frontend/src/components/modals/addDeviceModal.tsx create mode 100644 frontend/src/components/modals/addUserModal.tsx create mode 100644 frontend/src/components/modals/confirmDeleteModal.tsx create mode 100644 frontend/src/components/modals/userModal.tsx create mode 100644 frontend/src/components/modelPanel.tsx create mode 100644 frontend/src/components/periodSelector.tsx create mode 100644 frontend/src/components/statCard.tsx create mode 100644 frontend/src/components/statsBar.tsx create mode 100644 frontend/src/components/userCard.tsx create mode 100644 frontend/src/config/config.ts create mode 100644 frontend/src/helpers/chartToolTip.tsx create mode 100644 frontend/src/helpers/helpers.ts create mode 100644 frontend/src/helpers/toolTip.tsx create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/pages/DeviceAdmin.tsx create mode 100644 frontend/src/pages/UserBoard.tsx create mode 100644 frontend/src/types/Types.ts 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 8ed1875..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 ( -
- - - {open && ( -
-
-
- {/* All toggle */} - -
- {devices.map((device) => { - const checked = selected.includes(device.name); - const color = deviceColorMap[device.name]; - return ( - - ); - })} -
-
- )} -
- ); -} - -// ─── 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) => ( - - ))} -
- ); -} - -// ─── 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("/api/v1/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(`/api/v1/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("/api/v1/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("/api/v1/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(`/api/v1/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("/api/v1/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} - -
- ); - - if (phase.kind === "error") return ( -
-
- - Training failed - -
-

{phase.text}

-
- ); - - return null; - } - - return ( -
- - - {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:

-
-
- - setTrainHours(e.target.value)} - className="w-20 bg-slate-800/70 border border-slate-700/70 text-white rounded-lg px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-cyan-500/50 disabled:opacity-40" /> -
-
- - setTrainNote(e.target.value)} - className="w-full bg-slate-800/70 border border-slate-700/70 text-white placeholder-slate-600 rounded-lg px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-cyan-500/50 disabled:opacity-40" /> -
-
-
- - {hasModel && !isTraining && ( - - )} -
-
- - -
- )} -
- ); -} - -// ─── 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("/api/v1/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/v1/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 ── */} -
- - -
-
- -
-

Monitoring

-
- - - - - -
- - {lastFetched && ( - - - {lastFetched.toLocaleTimeString()} - - )} - -
- - {/* ── 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 2dd3436..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. -

- -
- - -
-
-
-
- ); -} - -// ─── 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("/api/v1/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

- -
- -
-
- - { - 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}

- )} -
-
- - { - 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 - -
-
- - {error && ( -

- {error} -

- )} - -
- - -
-
-
-
- ); -} - -// ─── 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} - -
- -
-
- - View dashboard -
- - - - - -
-
-
- ); -} - -// ─── 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("/api/v1/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(`/api/v1/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()} - - )} - - -
-
-
- - {/* ── Stats ── */} - {!loading && !error && } - - {/* ── Filter tabs ── */} -
- {(["all", "online", "offline"] as const).map((f) => ( - - ))} -
- - {/* ── Content ── */} - {loading ? ( -
-
-

Connecting to API…

-
- ) : error ? ( -
-

Failed to load devices

-

{error}

- -
- ) : filtered.length === 0 ? ( -
-
- -
-

- {filter === "all" ? "No devices registered yet." : `No ${filter} devices.`} -

- {filter === "all" && ( - - )} -
- ) : ( -
- {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 0c78615..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/v1/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() {

-
-
-
- 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 0bc8a47..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/v1/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..bb46234 100644 --- a/frontend/src/api.tsx +++ b/frontend/src/api.tsx @@ -1,33 +1,34 @@ -const API = "http://localhost:30080"; +import axios, { type AxiosRequestConfig, AxiosError } from "axios"; +import { API, API_VERSION } from "./config/config"; -export async function apiFetch( +export async function apiFetch( endpoint: string, - options: RequestInit = {} -) { - const response = await fetch(API + endpoint, { - credentials: "include", - headers: { - "Content-Type": "application/json", - ...(options.headers ?? {}), - }, - ...options, - }); + options: AxiosRequestConfig = {} +): Promise { + try { + const response = await axios(API + API_VERSION + endpoint, { + withCredentials: true, + headers: { + "Content-Type": "application/json", + ...(options.headers ?? {}), + }, + ...options, + }); - if (response.status === 401) { - window.location.href = "/login"; - throw new Error("Unauthorized"); - } + return response.data as T; + } catch (err) { + const error = err as AxiosError; - if (!response.ok) { - let message = `HTTP ${response.status}`; + if (error.response?.status === 401) { + window.location.href = "/login"; + throw new Error("Unauthorized"); + } - try { - const data = await response.json(); - message = data.detail ?? data.message ?? message; - } catch {} + // const message = + // error.response?.data?.detail ?? + // error.response?.data?.message ?? + // (error.response ? `HTTP ${error.response.status}` : error.message); - throw new Error(message); + throw error; } - - return response; } \ 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} + +
+ +
+
+ + View dashboard +
+ + + + + +
+
+
+ ); +} \ 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 ( +
+ + + {open && ( +
+
+
+ {/* All toggle */} + +
+ {devices.map((device) => { + const checked = selected.includes(device.name); + const color = deviceColorMap[device.name]; + return ( + + ); + })} +
+
+ )} +
+ ); +} \ 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

+ +
+ +
+
+ + { + 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}

+ )} +
+
+ + { + 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 + +
+
+ + {error && ( +

+ {error} +

+ )} + +
+ + +
+
+
+
+ ); +} \ 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

+ +
+ +
+
+ + { + 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}

+ )} +
+
+ + { + 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 + +
+
+ + {error && ( +

+ {error} +

+ )} + +
+ + +
+
+
+
+ ); +} \ 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. +

+ +
+ + +
+
+
+
+ ); +} \ 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..676044a --- /dev/null +++ b/frontend/src/components/modals/userModal.tsx @@ -0,0 +1,347 @@ +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 = + 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 = + 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 = + 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} +

+ )} */} + +
+
+ {/* */} + {confirmingChangeRole ? ( +
+
+

Change role for this user?

+ {roleError && ( +

+ {roleError} +

+ )} +
+ + +
+
+
+ ) : ( + + )} + {/* */} + {modalNewUserPassword ? ( +
+
+

Change password for this user

+

+ This action cannot be undone. +

+ {passwordError && ( +

+ {passwordError} +

+ )} + + { + 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"} + /> +
+ + +
+
+
+ ) : ( + + )} +
+
+ + {confirmingDelete ? ( +
+
+

Delete this user?

+

+ This action cannot be undone. +

+ {deleteError && ( +

+ {deleteError} +

+ )} +
+ + +
+
+
+ ) : ( + + )} +
+
+
+
+
+ ); +} \ 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..496c0a2 --- /dev/null +++ b/frontend/src/components/modelPanel.tsx @@ -0,0 +1,299 @@ +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.response?.status === 403) { + setPhase({ kind: "error", text: "Training 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: `Network error: ${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} + +
+ ); + + if (phase.kind === "error") return ( +
+
+ + Training failed + +
+

{phase.text}

+
+ ); + + return null; + } + + return ( +
+ + + {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:

+
+
+ + setTrainHours(e.target.value)} + className="w-20 bg-slate-800/70 border border-slate-700/70 text-white rounded-lg px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-cyan-500/50 disabled:opacity-40" /> +
+
+ + setTrainNote(e.target.value)} + className="w-full bg-slate-800/70 border border-slate-700/70 text-white placeholder-slate-600 rounded-lg px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-cyan-500/50 disabled:opacity-40" /> +
+
+
+ + {hasModel && !isTraining && ( + + )} +
+
+ + +
+ )} +
+ ); +} \ 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) => ( + + ))} +
+ ); +} \ 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 ── */} +
+ + +
+
+ +
+

Monitoring

+
+ + + + + +
+ + {lastFetched && ( + + + {lastFetched.toLocaleTimeString()} + + )} + +
+ + {/* ── 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()} + + )} + + + + +
+
+
+ + {/* ── Stats ── */} + {!loading && !error && } + + {/* ── Filter tabs ── */} +
+ {(["all", "online", "offline"] as const).map((f) => ( + + ))} +
+ + {/* ── Content ── */} + {loading ? ( +
+
+

Connecting to API…

+
+ ) : error ? ( +
+

Failed to load devices

+

{error}

+ +
+ ) : filtered.length === 0 ? ( +
+
+ +
+

+ {filter === "all" ? "No devices registered yet." : `No ${filter} devices.`} +

+ {filter === "all" && ( + + )} +
+ ) : ( +
+ {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..95cb869 --- /dev/null +++ b/frontend/src/pages/UserBoard.tsx @@ -0,0 +1,151 @@ +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.response?.status === 403) { + setError("This panel 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"; + setError(message); + } + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchUsers(); + const interval = setInterval(fetchUsers, 15000); + return () => clearInterval(interval); + }, [fetchUsers]); + + return ( +
+
+
+ +
+
+
+ +
+
+ + + Users control + +
+

+ Users Registry +

+

+ Manage Users +

+
+
+ +
+ +
+
+ + {/* Content */} + { + loading ? ( +
+
+

Connecting to API…

+
+ ) : error ? ( +
+

Failed to load devices

+

{error}

+ +
+ ) : ( +
+ {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 From 1e985b38d7518c0899d4230b021768961cca77ba Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Thu, 27 Aug 2026 19:46:15 +0300 Subject: [PATCH 06/35] added scripts for deploying and docker-compose files --- Dockerfile.dev | 23 +++++ Makefile | 7 +- deploy.ps1 | 7 +- docker-compose.dev.yml | 73 ++++++++++++++ docker-compose.prod.yml | 201 ++++++++++++++++++++++++++++++++++++++ env.example | 15 +++ prometheus/prometheus.yml | 23 +++++ 7 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 env.example create mode 100644 prometheus/prometheus.yml diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..814e928 --- /dev/null +++ b/Dockerfile.dev @@ -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/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/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/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..51ac7a4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,73 @@ +services: + postgres: + image: postgres:16-alpine + container_name: mona-postgres-dev + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-myuser} -d ${POSTGRES_DB:-mydb}"] + 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: Dockerfile.dev + container_name: mona-fastapi-dev + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + env_file: + - .env.dev + environment: + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + REDIS_URL: redis://redis:6379 + CORS_ORIGINS: '["http://localhost:30080","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" + +volumes: + pgdata: + redisdata: + api_venv: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..b5ea2ae --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,201 @@ +services: + # ─── PostgreSQL ───────────────────────────────────────────────────────── + postgres: + image: postgres:16-alpine + container_name: mona-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + 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:-myuser} -d ${POSTGRES_DB:-mydb}"] + 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: + 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: "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: + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + POSTGRES_HOST: postgres + 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: + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + POSTGRES_HOST: postgres + 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: + redisdata: + prometheus-data: + celerybeat-tmp: + +networks: + mona: + driver: bridge \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..1daf637 --- /dev/null +++ b/env.example @@ -0,0 +1,15 @@ +# Copy to .env and fill in real values before running docker compose up. + +POSTGRES_USER=myuser +POSTGRES_PASSWORD=changeme +POSTGRES_DB=mydb + +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/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 From 7c15dc05f7f530b330b0a9cc8d0619e59840c8b8 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Thu, 27 Aug 2026 20:01:32 +0300 Subject: [PATCH 07/35] cleared comments --- .github/workflows/ci.yml | 6 ++-- .yamllint.yaml | 7 ++++- mona-chart/templates/postgres-deployment.yaml | 19 ------------ mona-chart/values.yaml | 1 - terraform/main.tf | 31 ------------------- 5 files changed, 9 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc05506..0061bd2 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 diff --git a/.yamllint.yaml b/.yamllint.yaml index 5b87200..c4eef45 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -1,5 +1,9 @@ extends: default +ignore: | + mona-chart/templates/ + .github/workflows/ + rules: line-length: max: 200 @@ -9,4 +13,5 @@ rules: document-start: disable new-line-at-end-of-file: disable new-lines: disable - trailing-spaces: disable \ No newline at end of file + trailing-spaces: disable + comments-indentation: disable \ No newline at end of file diff --git a/mona-chart/templates/postgres-deployment.yaml b/mona-chart/templates/postgres-deployment.yaml index bdf5648..ba68095 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 }}" diff --git a/mona-chart/values.yaml b/mona-chart/values.yaml index 8b5da54..568fc3e 100644 --- a/mona-chart/values.yaml +++ b/mona-chart/values.yaml @@ -358,7 +358,6 @@ kube-prometheus-stack: type: NodePort nodePort: 30091 prometheusSpec: - # Отключаем привязку к helm-релизу serviceMonitorSelectorNilUsesHelmValues: false serviceMonitorSelector: {} # External node exporter for monitoring physical PC metrics. diff --git a/terraform/main.tf b/terraform/main.tf index adb47dc..98a7fc0 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,27 +132,8 @@ 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 = "postgres.auth.user" value = var.postgres_user @@ -211,7 +181,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 From 0e3bcbc8afb5985ac50b13f5b3a4cfee00231659 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 22:09:46 +0300 Subject: [PATCH 08/35] dev update --- CONTRIBUTING.md | 5 + Dockerfile.dev => dev/Dockerfile.dev.fastapi | 0 dev/Dockerfile.dev.react | 10 + dev/docker-compose.dev.yml | 269 +++++++++++++++++++ docker-compose.dev.yml | 73 ----- docker-compose.prod.yml | 18 +- 6 files changed, 294 insertions(+), 81 deletions(-) rename Dockerfile.dev => dev/Dockerfile.dev.fastapi (100%) create mode 100644 dev/Dockerfile.dev.react create mode 100644 dev/docker-compose.dev.yml delete mode 100644 docker-compose.dev.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 920253e..e32058d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,11 @@ uv run pre-commit run --hook-stage pre-push --all-files 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 diff --git a/Dockerfile.dev b/dev/Dockerfile.dev.fastapi similarity index 100% rename from Dockerfile.dev rename to dev/Dockerfile.dev.fastapi 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..1fa1703 --- /dev/null +++ b/dev/docker-compose.dev.yml @@ -0,0 +1,269 @@ +# 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 +# container_name: mona-fastapi-dev +# restart: unless-stopped +# depends_on: +# postgres: +# condition: service_healthy +# redis: +# condition: service_healthy +# env_file: +# - .env.dev +# environment: +# # POSTGRES_USER: ${POSTGRES_USER:-myuser} +# # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} +# # POSTGRES_DB: ${POSTGRES_DB:-mydb} +# POSTGRES_HOST: postgres +# POSTGRES_PORT: 5432 +# REDIS_URL: redis://redis:6379 +# CORS_ORIGINS: '["http://localhost:30080","http://localhost:30081", "http://localhost:5173"]' +# 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" + +# 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" + +# 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 + +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: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + REDIS_URL: redis://redis:6379 + CORS_ORIGINS: '["http://localhost:30080","http://localhost:30081","http://localhost:5173"]' + 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: + PYTHONPATH: /app/mona_core + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + REDIS_URL: redis://redis:6379 + PROMETHEUS_URL: http://prometheus:9090 + 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: + PYTHONPATH: /app/mona_core + POSTGRES_HOST: postgres + 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/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 51ac7a4..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,73 +0,0 @@ -services: - postgres: - image: postgres:16-alpine - container_name: mona-postgres-dev - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-myuser} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} - POSTGRES_DB: ${POSTGRES_DB:-mydb} - ports: - - "5432:5432" - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-myuser} -d ${POSTGRES_DB:-mydb}"] - 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: Dockerfile.dev - container_name: mona-fastapi-dev - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - env_file: - - .env.dev - environment: - POSTGRES_USER: ${POSTGRES_USER:-myuser} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} - POSTGRES_DB: ${POSTGRES_DB:-mydb} - POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 - REDIS_URL: redis://redis:6379 - CORS_ORIGINS: '["http://localhost:30080","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" - -volumes: - pgdata: - redisdata: - api_venv: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b5ea2ae..68d02ac 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,16 +5,16 @@ services: container_name: mona-postgres restart: unless-stopped environment: - POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} - POSTGRES_DB: ${POSTGRES_DB:-mydb} + 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:-myuser} -d ${POSTGRES_DB:-mydb}"] + 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 @@ -52,11 +52,11 @@ services: condition: service_healthy environment: 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: "5432" + # POSTGRES_USER: ${POSTGRES_USER:-myuser} + # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + # POSTGRES_DB: ${POSTGRES_DB:-mydb} + # POSTGRES_HOST: postgres + # POSTGRES_PORT: "5432" REDIS_URL: redis://redis:6379 CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]} ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin} @@ -192,7 +192,9 @@ services: volumes: pgdata: + name: mona_pgdata_prod redisdata: + name: mona_redisdata_prod prometheus-data: celerybeat-tmp: From 162f3125767c380e90c42e6796c29b64dcb17fd9 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 22:10:18 +0300 Subject: [PATCH 09/35] frontend update --- frontend/src/api.tsx | 39 +++++++++++++++++--- frontend/src/components/modals/userModal.tsx | 25 +++++++------ frontend/src/components/modelPanel.tsx | 10 +---- frontend/src/pages/UserBoard.tsx | 16 +++----- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/frontend/src/api.tsx b/frontend/src/api.tsx index bb46234..79837a0 100644 --- a/frontend/src/api.tsx +++ b/frontend/src/api.tsx @@ -1,6 +1,31 @@ import axios, { type AxiosRequestConfig, AxiosError } from "axios"; import { API, API_VERSION } from "./config/config"; +export class ApiError extends Error { + public status?: number; + + constructor(message: string, status?: number) { + super(message); + this.status = status; + } +} + +function extractErrorMessage(error: AxiosError): string { + const data = error.response?.data; + + 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; + + if (Array.isArray(data?.detail) && data.detail[0]?.msg) { + return data.detail[0].msg; + } + + return error.response + ? `HTTP ${error.response.status}` + : error.message ?? "Network error"; +} + export async function apiFetch( endpoint: string, options: AxiosRequestConfig = {} @@ -19,16 +44,18 @@ export async function apiFetch( } catch (err) { const error = err as AxiosError; - if (error.response?.status === 401) { + const isLoginRequest = endpoint.includes("/auth/login"); + + if (error.response?.status === 401 && !isLoginRequest) { window.location.href = "/login"; throw new Error("Unauthorized"); } - // const message = - // error.response?.data?.detail ?? - // error.response?.data?.message ?? - // (error.response ? `HTTP ${error.response.status}` : error.message); + // throw new Error(extractErrorMessage(error)); - throw error; + throw new ApiError( + extractErrorMessage(error), + error.response?.status + ); } } \ No newline at end of file diff --git a/frontend/src/components/modals/userModal.tsx b/frontend/src/components/modals/userModal.tsx index 676044a..22d7e32 100644 --- a/frontend/src/components/modals/userModal.tsx +++ b/frontend/src/components/modals/userModal.tsx @@ -35,7 +35,7 @@ export function UserModal({ user, onClose, fetchUsers }: UserModalProps) { } return true; }; - + const handleUserDelete = async () => { setLoading(true); setDeleteError(""); @@ -60,9 +60,10 @@ export function UserModal({ user, onClose, fetchUsers }: UserModalProps) { } const message = - typeof d.detail === "string" - ? d.detail - : d.detail?.message ?? e.message ?? "Unknown error"; + d.message ?? + (typeof d.detail === "string" ? d.detail : d.detail?.message) ?? + e.message ?? + "Unknown error"; setDeleteError(message); } finally { setLoading(false); @@ -96,9 +97,10 @@ export function UserModal({ user, onClose, fetchUsers }: UserModalProps) { } } const message = - typeof d.detail === "string" - ? d.detail - : d.detail?.message ?? e.message ?? "Unknown error"; + d.message ?? + (typeof d.detail === "string" ? d.detail : d.detail?.message) ?? + e.message ?? + "Unknown error"; setRoleError(message); } finally { setLoading(false); @@ -107,7 +109,7 @@ export function UserModal({ user, onClose, fetchUsers }: UserModalProps) { const handleUserChangePassword = async () => { if (!validate_password()) return; - + setLoading(true); setPasswordError(""); try { @@ -134,9 +136,10 @@ export function UserModal({ user, onClose, fetchUsers }: UserModalProps) { } } const message = - typeof d.detail === "string" - ? d.detail - : d.detail?.message ?? e.message ?? "Unknown error"; + d.message ?? + (typeof d.detail === "string" ? d.detail : d.detail?.message) ?? + e.message ?? + "Unknown error"; setPasswordError(message); } finally { setLoading(false); diff --git a/frontend/src/components/modelPanel.tsx b/frontend/src/components/modelPanel.tsx index 496c0a2..957e86f 100644 --- a/frontend/src/components/modelPanel.tsx +++ b/frontend/src/components/modelPanel.tsx @@ -120,16 +120,10 @@ export function ModelPanel({ onTrained }: { onTrained: () => void }) { setPhase({ kind: "error", text: d.message ?? "Unknown error from server" }); } } catch (e: any) { - if (e.response?.status === 403) { + if (e.status === 403) { setPhase({ kind: "error", text: "Training 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: `Network error: ${message}` }); + setPhase({ kind: "error", text: `error: ${e.message}`}) } } diff --git a/frontend/src/pages/UserBoard.tsx b/frontend/src/pages/UserBoard.tsx index 95cb869..1251fad 100644 --- a/frontend/src/pages/UserBoard.tsx +++ b/frontend/src/pages/UserBoard.tsx @@ -22,16 +22,10 @@ export default function UserBoard() { setUsers(data); setError(""); } catch (e: any) { - if (e.response?.status === 403) { - setError("This panel requires admin privileges"); + if (e.status === 403) { + setError("This page 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"; - setError(message); + setError(e.message); } } finally { setLoading(false); @@ -67,9 +61,9 @@ export default function UserBoard() {
- +
Users control From 7301117c5f5ae4306f79e4c2ced856dcf3644fd5 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 22:33:20 +0300 Subject: [PATCH 10/35] API update --- CHANGELOG.md | 30 ++- README.md | 47 +++- mona_core/celery_conf.py | 5 - mona_core/config.py | 13 +- mona_core/db.py | 5 +- mona_core/main.py | 379 +++----------------------------- mona_core/ml.py | 6 - mona_core/routers/__init__.py | 0 mona_core/routers/dashboard.py | 56 +++++ mona_core/routers/devices.py | 70 ++++++ mona_core/routers/health.py | 40 ++++ mona_core/routers/model.py | 119 ++++++++++ mona_core/routers/monitoring.py | 95 ++++++++ mona_core/routers/tasks.py | 19 ++ mona_core/routers/users.py | 169 ++++++++++++++ mona_core/schemas.py | 176 +++++++++++++++ mona_core/security.py | 110 ++++++--- mona_core/tasks.py | 5 +- 18 files changed, 920 insertions(+), 424 deletions(-) create mode 100644 mona_core/routers/__init__.py create mode 100644 mona_core/routers/dashboard.py create mode 100644 mona_core/routers/devices.py create mode 100644 mona_core/routers/health.py create mode 100644 mona_core/routers/model.py create mode 100644 mona_core/routers/monitoring.py create mode 100644 mona_core/routers/tasks.py create mode 100644 mona_core/routers/users.py create mode 100644 mona_core/schemas.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4a75e..c1272d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,34 @@ # CHANGELOG All notable changes to this project will be documented in this file. -## [v0.7.0] - 2026-07-29 - API update +## [v0.7.0] - 2026-08-28 - Major update ### Added -- Added api versioning except probes, prometheus endpoints. +- 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. +- frontend: Added page `userboard`. +- frontend: Added config with `API_URL` and `API_VERSION`. +- frontend: Added formatter for `status codes` and `error` into `apiFetch`. + +### Changed +- 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/README.md b/README.md index c9852da..e776d7f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ ![CI Pipeline](https://img.shields.io/badge/CI_Pipeline-Automated_Checks-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) ![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white) ![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) +![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) ![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=for-the-badge&logo=kubernetes&logoColor=white) ![Terraform](https://img.shields.io/badge/Terraform-844FBA?style=for-the-badge&logo=terraform&logoColor=white) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?style=for-the-badge&logo=postgresql&logoColor=white) @@ -15,6 +17,7 @@ MONA is a K8s-based monitoring and analytics tool managed with Terraform and Hel ## 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` @@ -33,11 +36,23 @@ 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(Monitoring devices):** `localhost:30391/` * **Prometheus(Cluster):** `localhost:30091/` @@ -48,16 +63,22 @@ 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` +* Metrics[Get]: `localhost:30080/api/v1/db-metrics` +* 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` +* UserChangeRole[Patch]: `localhost:30080/api/v1/user/{user_id}/role` +* Devices[Get]: `localhost:30080/api/v1/devices` +* Devices[Post]: `localhost:30080/api/v1/devices` +* Devices[Delete]: `localhost:30080/api/v1/devices/{device_id}` * Anomalies[Get]: `localhost:30080/api/v1/anomalies` * Model-info[Get]: `localhost:30080/api/v1/model-info` -* Devices[Get]: `localhost:30080/api/v1/devices` -* Devices[Post]: `localhost:30080/api/v1/devices` *available via curl* -* Devices[Delete]: `localhost:30080/api/v1/devices/{device_id}` *available via curl* -* Train[Post]: `localhost:30080/api/v1/train` *available via curl* -* Model[Delete]: `localhost:30080/api/v1/model` *available via curl* +* Train[Post]: `localhost:30080/api/v1/train` +* Model[Delete]: `localhost:30080/api/v1/model` * Dashboard[Get]: `localhost:30080/api/v1/dashboard` +* TaskStatus[Get]: `localhost:30080/api/v1/task-status/{task_id}` + #### Probes: * Liveness[Get]: `localhost:30080/health/live` * Readiness[Get]: `localhost:30080/health/ready` @@ -65,7 +86,11 @@ make all #### Authentication: * Login[Post]: `localhost:30080/api/v1/auth/login` * Logout[Post]: `localhost:30080/api/v1/auth/logout` -* Check auth[Get]: `localhost:30080/api/v1auth/me` +* Check auth[Get]: `localhost:30080/api/v1/auth/me` + +#### Prometheus Endpoints: +* Metrics[Get]: `localhost:30080/metrics` +* Targets[Get]: `localhost:30080/api/prometheus/targets` @@ -78,7 +103,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. ![dashboard](https://github.com/gwill1337/Images/blob/main/MONA/dashboard.gif) diff --git a/mona_core/celery_conf.py b/mona_core/celery_conf.py index 70e47a2..fde398f 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,8 +11,6 @@ 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}" diff --git a/mona_core/config.py b/mona_core/config.py index 61d74d7..227a4e5 100644 --- a/mona_core/config.py +++ b/mona_core/config.py @@ -1,14 +1,17 @@ +from celery import Celery from pydantic import computed_field from pydantic_settings import BaseSettings, SettingsConfigDict +from redis.asyncio import from_url class Settings(BaseSettings): # 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 +41,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/main.py b/mona_core/main.py index dce1464..41cfcfa 100644 --- a/mona_core/main.py +++ b/mona_core/main.py @@ -1,29 +1,21 @@ 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.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() yield @@ -38,355 +30,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("/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, prefix="/api/v1") -app.include_router(user_router, prefix="/api/v1") +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..45b4e3e 100644 --- a/mona_core/ml.py +++ b/mona_core/ml.py @@ -25,11 +25,6 @@ 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 - # ─── Helpers ──────────────────────────────────────────────────────────────── @@ -101,7 +96,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 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..844bc35 --- /dev/null +++ b/mona_core/routers/dashboard.py @@ -0,0 +1,56 @@ +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..7b5717d --- /dev/null +++ b/mona_core/routers/devices.py @@ -0,0 +1,70 @@ +from typing import Sequence + +from fastapi import Depends, HTTPException, Query, status +from sqlalchemy import delete, select +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 + + +@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: + 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=status.HTTP_409_CONFLICT, + detail="Name already exists", + ) + db.refresh(dev) + 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: + 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() + + return MessageResponse(message="Device deleted") + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Database error: {str(e)}", + ) 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..57aa3eb --- /dev/null +++ b/mona_core/routers/model.py @@ -0,0 +1,119 @@ +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, +) + + +@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 as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) 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..60a7aa9 --- /dev/null +++ b/mona_core/routers/users.py @@ -0,0 +1,169 @@ +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, +) + + +@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: + 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() + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="User already exists", + ) + + 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: + 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 as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + + await remove_all_sessions(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: + # log Warn + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Username or Password invalid", + ) + res.set_password(body.new_password) + try: + db.commit() + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + 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: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"status": "error", "message": "User not found"}, + ) + + if user.role == body.role: + 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: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot demote the last remaining admin", + ) + elif user.username == current_user.username: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You cannot change your own role", + ) + + user.role = body.role + try: + db.commit() + db.refresh(user) + except Exception: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Server error", + ) + return MessageResponse(message="Role switched") diff --git a/mona_core/schemas.py b/mona_core/schemas.py new file mode 100644 index 0000000..bbc1983 --- /dev/null +++ b/mona_core/schemas.py @@ -0,0 +1,176 @@ +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): + 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 35db4bf..ef09d08 100644 --- a/mona_core/security.py +++ b/mona_core/security.py @@ -2,18 +2,19 @@ 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 - -# redis_url = os.getenv("REDIS_URL", "redis://redis:6379") -redis_url = settings.redis_url -redis_client = from_url(redis_url, decode_responses=True) +from mona_core.schemas import ( + LoginOut, + LoginRequest, + MeOut, + MessageResponse, + UserSession, +) # ─── helpers ──────────────────────────────────────────────────────────────── @@ -25,9 +26,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]) + await redis_client.delete(key) # ─── Startup ──────────────────────────────────────────────────────────────── @@ -61,7 +82,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" ) @@ -80,7 +101,7 @@ def seed_admin(): print(f"Error: {e}") -# limiter +# ─── limiter ──────────────────────────────────────────────────────────────── async def check_rate_limit( key: str, max_attempts: int = settings.max_attempts, @@ -103,39 +124,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("/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 +175,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 +187,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("/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("/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..72da0e8 100644 --- a/mona_core/tasks.py +++ b/mona_core/tasks.py @@ -2,12 +2,11 @@ from datetime import UTC, datetime, timedelta import requests - -# import pickle 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 # ─── Helpers ──────────────────────────────────────────────────────────────── @@ -130,7 +129,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) From 37c6b3b74fc39fb39f8d4248f1a3c3eb90ff2356 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:10:44 +0300 Subject: [PATCH 11/35] fix mypy --- mona_core/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mona_core/security.py b/mona_core/security.py index ef09d08..ae0b60c 100644 --- a/mona_core/security.py +++ b/mona_core/security.py @@ -47,7 +47,7 @@ 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]) + await redis_client.delete(*[f"session:{sid}" for sid in session_ids]) # type: ignore[str-bytes-safe] await redis_client.delete(key) From 6903b0d64dbb8dc0f7e6af7ee1617c21dee23300 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:10:54 +0300 Subject: [PATCH 12/35] fix --- .dockerignore | 2 ++ .gitignore | 2 ++ dev/.env.dev.example | 8 ++++++++ 3 files changed, 12 insertions(+) create mode 100644 dev/.env.dev.example diff --git a/.dockerignore b/.dockerignore index ae7e651..dce99b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -37,6 +37,8 @@ conftest.py **/terraform.tfvars* **/*.tfvars **/.env +**/.env.dev +dev/.env.dev deploy.ps1 Makefile 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/dev/.env.dev.example b/dev/.env.dev.example new file mode 100644 index 0000000..5703ce9 --- /dev/null +++ b/dev/.env.dev.example @@ -0,0 +1,8 @@ +POSTGRES_USER=myuser123 +POSTGRES_PASSWORD=changePassword +POSTGRES_DB=mydb + +ADMIN_USERNAMES=admin +ADMIN_PASSWORDS=admin123 +USER_USERNAMES=user1 +USER_PASSWORDS=userpass1 From 6c8f9d2069b1a600ee855daf068ec5723d4a0e7b Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:11:04 +0300 Subject: [PATCH 13/35] tests update --- tests/conftest.py | 90 ++++- tests/test_anomalies.py | 73 ++++ tests/test_api.py | 674 -------------------------------- tests/test_auth.py | 306 +++++++++++++++ tests/test_dashboard_metrics.py | 60 +++ tests/test_devices.py | 64 +++ tests/test_model.py | 76 ++++ tests/test_probes.py | 23 ++ tests/test_prometheus.py | 24 ++ tests/test_tasks.py | 55 +++ tests/test_users.py | 209 ++++++++++ 11 files changed, 964 insertions(+), 690 deletions(-) create mode 100644 tests/test_anomalies.py delete mode 100644 tests/test_api.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_dashboard_metrics.py create mode 100644 tests/test_devices.py create mode 100644 tests/test_model.py create mode 100644 tests/test_probes.py create mode 100644 tests/test_prometheus.py create mode 100644 tests/test_users.py diff --git a/tests/conftest.py b/tests/conftest.py index 69f370b..495588d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,8 @@ 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.routers import health as health_module # noqa: E402 +from mona_core.config import celery_client # noqa: E402 @pytest.fixture(scope="session", autouse=True) @@ -74,37 +76,59 @@ 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 + class FakeRedis: def __init__(self): @@ -141,9 +165,43 @@ async def ttl(self, key): if key in self.storage: return 300 return -2 + + async def ping(self): + return True + + async def sadd(self, name, *values): + if name not in self.storage: + self.storage[name] = set() + elif not isinstance(self.storage[name], set): + self.storage[name] = set(self.storage[name]) + + added = 0 + for val in values: + if val not in self.storage[name]: + self.storage[name].add(val) + added += 1 + return added + + async def srem(self, key, *values): + s = self.storage.get(key) + if not isinstance(s, set): + return 0 + before = len(s) + s.difference_update(values) + return before - len(s) + + async def smembers(self, key): + s = self.storage.get(key) + return set(s) if isinstance(s, set) else set() + + async def scard(self, key): + s = self.storage.get(key) + return len(s) if isinstance(s, set) else 0 + @pytest.fixture() def mock_redis(monkeypatch): fake = FakeRedis() monkeypatch.setattr(security_module, "redis_client", fake) + monkeypatch.setattr(health_module, "redis_client", fake) return fake 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 65f9384..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("/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 - 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("/api/v1/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("/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()["detail"] == "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() == {"message": "Device deleted successfully"} - - 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()["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("/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 - assert resp.json()["name"] == name - - 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 - 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("/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 - 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("/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 - - -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"] == [] - - -class TestTasks: - 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 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("/api/v1/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("/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" - - -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) - - -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/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()["detail"] == "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 - - 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"} - ) - - assert any("user:admin" in key or "ip:" in key for key in mock_redis.storage) - - resp = client.post( - "/api/v1/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/v1/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..ca4b550 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,306 @@ +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 + + 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"} + ) + + assert any("user:admin" in key or "ip:" in key for key in mock_redis.storage) + + resp = client.post( + "/api/v1/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/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_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..8d7a00e 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -264,3 +264,58 @@ def test_load_failure( assert result["status"] == "error" assert "skops 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..0d9d561 --- /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 or Password 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 From 97b0bd49db511eb8bb565db356eb99c7e4de523c Mon Sep 17 00:00:00 2001 From: Gwill1337 Date: Fri, 28 Aug 2026 23:30:48 +0300 Subject: [PATCH 14/35] Rename dockerfile.celery to Dockerfile.celery --- docker/{dockerfile.celery => Dockerfile.celery} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docker/{dockerfile.celery => Dockerfile.celery} (99%) diff --git a/docker/dockerfile.celery b/docker/Dockerfile.celery similarity index 99% rename from docker/dockerfile.celery rename to docker/Dockerfile.celery index 0d7ea95..00d432c 100644 --- a/docker/dockerfile.celery +++ b/docker/Dockerfile.celery @@ -14,4 +14,4 @@ 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 +CMD ["celery", "-A", "celery_conf", "worker", "--loglevel=info"] From bb528ba1e30ba2004fafe812998e568684f274f7 Mon Sep 17 00:00:00 2001 From: Gwill1337 Date: Fri, 28 Aug 2026 23:30:59 +0300 Subject: [PATCH 15/35] Rename dockerfile.fastapi to Dockerfile.fastapi --- docker/{dockerfile.fastapi => Dockerfile.fastapi} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docker/{dockerfile.fastapi => Dockerfile.fastapi} (98%) diff --git a/docker/dockerfile.fastapi b/docker/Dockerfile.fastapi similarity index 98% rename from docker/dockerfile.fastapi rename to docker/Dockerfile.fastapi index 690ae6f..cec30d8 100644 --- a/docker/dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -12,4 +12,4 @@ 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 +CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"] From a06d67318724e0b9b022419fa30cf44e66d02bb7 Mon Sep 17 00:00:00 2001 From: Gwill1337 Date: Fri, 28 Aug 2026 23:31:09 +0300 Subject: [PATCH 16/35] Rename dockerfile.react to Dockerfile.react --- docker/{dockerfile.react => Dockerfile.react} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docker/{dockerfile.react => Dockerfile.react} (93%) diff --git a/docker/dockerfile.react b/docker/Dockerfile.react similarity index 93% rename from docker/dockerfile.react rename to docker/Dockerfile.react index 63a273a..e3cff45 100644 --- a/docker/dockerfile.react +++ b/docker/Dockerfile.react @@ -31,4 +31,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;"] From 3f3bee1ef8634a25c418651258ec30ec2888a726 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:38:22 +0300 Subject: [PATCH 17/35] fix Dockerfile.fastapi --- .github/workflows/ci.yml | 2 +- dev/Dockerfile.dev.fastapi | 34 ++++++++++++++-------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0061bd2..af577af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,4 +161,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/dev/Dockerfile.dev.fastapi b/dev/Dockerfile.dev.fastapi index 814e928..8b0f636 100644 --- a/dev/Dockerfile.dev.fastapi +++ b/dev/Dockerfile.dev.fastapi @@ -1,23 +1,17 @@ -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 - +FROM python:3.13 AS builder WORKDIR /app +COPY docker/requirements_fastapi.txt . +RUN pip install --no-cache-dir -r requirements_fastapi.txt \ + && pip install --no-cache-dir --upgrade setuptools>=78.1.1 msgpack>=1.2.1 -# кэшируем зависимости отдельным слоем -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 +FROM python:3.13-slim +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* -CMD ["uv", "run", "uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +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 From 706619ede357d80f015c1b5632aff74821b3153b Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:40:48 +0300 Subject: [PATCH 18/35] fix --- dev/Dockerfile.dev.fastapi | 34 ++++++++++++++++++++-------------- docker/Dockerfile.fastapi | 8 +++++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/dev/Dockerfile.dev.fastapi b/dev/Dockerfile.dev.fastapi index 8b0f636..814e928 100644 --- a/dev/Dockerfile.dev.fastapi +++ b/dev/Dockerfile.dev.fastapi @@ -1,17 +1,23 @@ -FROM python:3.13 AS builder -WORKDIR /app -COPY docker/requirements_fastapi.txt . -RUN pip install --no-cache-dir -r requirements_fastapi.txt \ - && pip install --no-cache-dir --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 --from=builder /usr/local /usr/local -COPY . /app +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 -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 + +# кэшируем зависимости отдельным слоем +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/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index cec30d8..8b0f636 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,10 +1,12 @@ FROM python:3.13 AS builder WORKDIR /app COPY docker/requirements_fastapi.txt . -RUN pip install --no-cache-dir -r requirements_fastapi.txt +RUN pip install --no-cache-dir -r requirements_fastapi.txt \ + && pip install --no-cache-dir --upgrade setuptools>=78.1.1 msgpack>=1.2.1 FROM python:3.13-slim -# RUN addgroup --system app && adduser --system --ingroup app app +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 @@ -12,4 +14,4 @@ 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"] +CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file From 989ee4d0325c0f54f13f42a3ed3f22b1e7bf3a6d Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:44:59 +0300 Subject: [PATCH 19/35] fix --- docker/Dockerfile.fastapi | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index 8b0f636..c92e3eb 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,17 +1,23 @@ FROM python:3.13 AS builder WORKDIR /app COPY docker/requirements_fastapi.txt . -RUN pip install --no-cache-dir -r requirements_fastapi.txt \ - && pip install --no-cache-dir --upgrade setuptools>=78.1.1 msgpack>=1.2.1 +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/* -RUN addgroup --system --gid 10001 app && adduser --system --uid 10001 --ingroup app app 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 + +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 From bcde074bbb3f869695aa6e85edfe86d7fa6ebad5 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Fri, 28 Aug 2026 23:56:58 +0300 Subject: [PATCH 20/35] fix --- docker/Dockerfile.fastapi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index c92e3eb..d23f60b 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -9,7 +9,7 @@ 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 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 From 42a3c2592374a42aac6c5124205db2f1c5edee28 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:06:52 +0300 Subject: [PATCH 21/35] fix --- .dockerignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index dce99b9..09ee252 100644 --- a/.dockerignore +++ b/.dockerignore @@ -51,4 +51,7 @@ 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 + +uv.lock +uv.toml \ No newline at end of file From dc6792113f13eadbd5486a91a957406d14d9188f Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:10:40 +0300 Subject: [PATCH 22/35] fix --- .dockerignore | 3 - uv.lock | 1481 ++++++++++++++++++++++++++++--------------------- 2 files changed, 839 insertions(+), 645 deletions(-) diff --git a/.dockerignore b/.dockerignore index 09ee252..12bc668 100644 --- a/.dockerignore +++ b/.dockerignore @@ -52,6 +52,3 @@ kube_prom_matches.txt kube_prom_values.txt loki_stack_matches.txt loki_stack_values.txt - -uv.lock -uv.toml \ No newline at end of file diff --git a/uv.lock b/uv.lock index 02bd12b..6dae379 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,84 +490,84 @@ 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]] @@ -545,55 +581,122 @@ wheels = [ [[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" }, @@ -641,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]] @@ -854,7 +954,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.139.0" +version = "0.141.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -863,18 +963,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/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/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.0" +version = "3.32.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +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/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, + { 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]] @@ -964,61 +1064,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]] @@ -1045,15 +1145,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]] @@ -1073,17 +1173,27 @@ 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/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/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] -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" } + +[[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/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/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]] @@ -1097,11 +1207,11 @@ wheels = [ [[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]] @@ -1220,51 +1330,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]] @@ -1281,23 +1428,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]] @@ -1542,7 +1689,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.2.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -1551,31 +1698,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]] @@ -1589,11 +1746,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]] @@ -1616,80 +1773,105 @@ wheels = [ [[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]] @@ -1721,11 +1903,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]] @@ -1765,7 +1947,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.6.1" +version = "4.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1774,9 +1956,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +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/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, + { 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]] @@ -1793,36 +1975,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]] @@ -2019,7 +2201,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2027,88 +2209,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]] @@ -2150,24 +2332,23 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" } +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/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" }, + { 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]] @@ -2236,11 +2417,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]] @@ -2426,54 +2607,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]] @@ -2535,43 +2718,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]] @@ -2628,11 +2831,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]] @@ -2657,48 +2860,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]] @@ -2760,14 +2957,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]] @@ -2820,15 +3017,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]] @@ -2842,7 +3039,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.7.0" +version = "21.7.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2850,18 +3047,18 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/25/e367a7229b0914772ca8d81b41fde012d9feda68523b52644a571bb21ce8/virtualenv-21.7.0.tar.gz", hash = "sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c", size = 5527510, upload-time = "2026-07-21T13:12:14.109Z" } +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/a5/7a/ae29312b1e88a22e81f5d21fc11526d2a114089776c2550d2b205b6c2a47/virtualenv-21.7.0-py3-none-any.whl", hash = "sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd", size = 5507078, upload-time = "2026-07-21T13:12:12.136Z" }, + { 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]] From 3dc1448296b95596354d55acbeb5d678076b3205 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:19:35 +0300 Subject: [PATCH 23/35] fix --- .github/workflows/ci.yml | 2 ++ dev/Dockerfile.dev.fastapi | 2 +- docker/Dockerfile.celery | 4 ++-- docker/Dockerfile.fastapi | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af577af..985ddc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/dev/Dockerfile.dev.fastapi b/dev/Dockerfile.dev.fastapi index 814e928..32e851e 100644 --- a/dev/Dockerfile.dev.fastapi +++ b/dev/Dockerfile.dev.fastapi @@ -1,4 +1,4 @@ -FROM python:3.13-slim +FROM python:3.13.7-slim RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential curl \ diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery index 00d432c..c4550ab 100644 --- a/docker/Dockerfile.celery +++ b/docker/Dockerfile.celery @@ -1,8 +1,8 @@ -FROM python:3.13 AS builder +FROM python:3.13.7 AS builder COPY docker/requirements_celery.txt . RUN pip install --no-cache-dir -r requirements_celery.txt -FROM python:3.13-slim +FROM python:3.13.7-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 diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index d23f60b..71cd7ae 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,9 +1,9 @@ -FROM python:3.13 AS builder +FROM python:3.13.7 AS builder WORKDIR /app COPY docker/requirements_fastapi.txt . RUN pip install --no-cache-dir -r requirements_fastapi.txt -FROM python:3.13-slim +FROM python:3.13.7-slim RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* From a4e543c960e4e71fdf46650c5ae3d08110635635 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:24:45 +0300 Subject: [PATCH 24/35] fix --- dev/Dockerfile.dev.fastapi | 2 +- docker/Dockerfile.celery | 4 ++-- docker/Dockerfile.fastapi | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/Dockerfile.dev.fastapi b/dev/Dockerfile.dev.fastapi index 32e851e..814e928 100644 --- a/dev/Dockerfile.dev.fastapi +++ b/dev/Dockerfile.dev.fastapi @@ -1,4 +1,4 @@ -FROM python:3.13.7-slim +FROM python:3.13-slim RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential curl \ diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery index c4550ab..00d432c 100644 --- a/docker/Dockerfile.celery +++ b/docker/Dockerfile.celery @@ -1,8 +1,8 @@ -FROM python:3.13.7 AS builder +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.7-slim +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 diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index 71cd7ae..d23f60b 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,9 +1,9 @@ -FROM python:3.13.7 AS builder +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.7-slim +FROM python:3.13-slim RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* From 50d06778bcc2eb0b5ac67918203f7a20f410c3e5 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:29:34 +0300 Subject: [PATCH 25/35] fix --- docker/Dockerfile.react | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/Dockerfile.react b/docker/Dockerfile.react index e3cff45..22283d1 100644 --- a/docker/Dockerfile.react +++ b/docker/Dockerfile.react @@ -1,6 +1,10 @@ # ---------- Build ---------- FROM node:20-alpine AS build +RUN apt-get update \ + && apt-get upgrade -y \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY frontend/package*.json ./ From fdfab291bb4f228b65e555dd4c53034dc5677e84 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:34:43 +0300 Subject: [PATCH 26/35] fix --- docker/Dockerfile.fastapi | 5 +++++ docker/Dockerfile.react | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index d23f60b..73463d2 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,4 +1,9 @@ FROM python:3.13 AS builder + +RUN apt-get update \ + && apt-get upgrade -y \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY docker/requirements_fastapi.txt . RUN pip install --no-cache-dir -r requirements_fastapi.txt diff --git a/docker/Dockerfile.react b/docker/Dockerfile.react index 22283d1..e3cff45 100644 --- a/docker/Dockerfile.react +++ b/docker/Dockerfile.react @@ -1,10 +1,6 @@ # ---------- Build ---------- FROM node:20-alpine AS build -RUN apt-get update \ - && apt-get upgrade -y \ - && rm -rf /var/lib/apt/lists/* - WORKDIR /app COPY frontend/package*.json ./ From 669af145e9c8ddc12f046814ebe5da6ab717329a Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 00:39:04 +0300 Subject: [PATCH 27/35] fix --- docker/Dockerfile.celery | 5 +++++ docker/Dockerfile.fastapi | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery index 00d432c..6fb9561 100644 --- a/docker/Dockerfile.celery +++ b/docker/Dockerfile.celery @@ -1,4 +1,9 @@ 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 diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index 73463d2..4bb7089 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,9 +1,5 @@ FROM python:3.13 AS builder -RUN apt-get update \ - && apt-get upgrade -y \ - && rm -rf /var/lib/apt/lists/* - WORKDIR /app COPY docker/requirements_fastapi.txt . RUN pip install --no-cache-dir -r requirements_fastapi.txt From 8121c52923d63c92301e2c1c8612c4b7a3b35bfe Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 01:18:11 +0300 Subject: [PATCH 28/35] fix --- docker/Dockerfile.celery | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery index 6fb9561..ae44543 100644 --- a/docker/Dockerfile.celery +++ b/docker/Dockerfile.celery @@ -8,7 +8,9 @@ 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 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 From 3727a9ed0cea8c2fc1a6b94813efd802138c2cf2 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Sat, 29 Aug 2026 01:23:06 +0300 Subject: [PATCH 29/35] fix --- docker/Dockerfile.react | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/Dockerfile.react b/docker/Dockerfile.react index e3cff45..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 From 42a0028ae357d1c479b8867753e6d549fc7597fd Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Mon, 31 Aug 2026 18:29:20 +0300 Subject: [PATCH 30/35] readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e776d7f..556c105 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,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 ``` - +![userboard](https://github.com/gwill1337/Images/blob/main/MONA/userboard.gif) ## Authentication & Authorization MONA uses a stateful, cookie-based authentication system backed by Redis to secure the admin panel and core API endpoints. From 00a18201b2eb32b6b4d48434401c8ba98e8f35d3 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Mon, 31 Aug 2026 18:29:33 +0300 Subject: [PATCH 31/35] docker compose fix --- docker-compose.prod.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 68d02ac..80c291d 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -52,11 +52,11 @@ services: condition: service_healthy environment: 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: "5432" + POSTGRES_USER: ${POSTGRES_USER:-myuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + POSTGRES_DB: ${POSTGRES_DB:-mydb} + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" REDIS_URL: redis://redis:6379 CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]} ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin} From 2730bf6669b3826ab53d344bcf5a248a59e54627 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Mon, 31 Aug 2026 19:03:34 +0300 Subject: [PATCH 32/35] tests update --- README.md | 22 +++++++++----- pyproject.toml | 4 +++ tests/conftest.py | 73 ++-------------------------------------------- tests/test_auth.py | 15 ++++++---- uv.lock | 33 +++++++++++++++++++++ 5 files changed, 63 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 556c105..104b8f2 100644 --- a/README.md +++ b/README.md @@ -63,31 +63,39 @@ docker compose -p mona-prod --env-file .env -f docker-compose.prod.yml up -d --b ### API: *P.S. All endpoint require authentification except prometheus and probes.* * Swagger: `localhost:30080/docs` -* Metrics[Get]: `localhost:30080/api/v1/db-metrics` + +#### 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` +* 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}` -* Anomalies[Get]: `localhost:30080/api/v1/anomalies` + +#### 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}` - -#### Probes: -* Liveness[Get]: `localhost:30080/health/live` -* Readiness[Get]: `localhost:30080/health/ready` +* 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` + #### Prometheus Endpoints: * Metrics[Get]: `localhost:30080/metrics` * Targets[Get]: `localhost:30080/api/prometheus/targets` diff --git a/pyproject.toml b/pyproject.toml index eabc4ae..fa3afbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ pythonpath = ["."] [dependency-groups] ci = [ "celery>=5.6.3", + "fakeredis>=2.37.1", "fastapi>=0.139.0", "httpx>=0.28.1", "httpx2>=2.5.0", @@ -55,6 +56,7 @@ ci = [ "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", ] @@ -78,4 +80,6 @@ dev = [ "scikit-learn>=1.9.0", "checkov>=3.3.8", "pre-commit>=4.6.1", + "fakeredis>=2.37.1", + "pytest-asyncio>=1.4.0", ] diff --git a/tests/conftest.py b/tests/conftest.py index 495588d..20c45e4 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") @@ -129,79 +130,9 @@ def _make(username="testuser", password="testpass123", role="user"): return _make - -class FakeRedis: - def __init__(self): - self.storage = {} - - async def set(self, key, value, ex=None): - self.storage[key] = value - return True - - async def get(self, key): - return self.storage.get(key) - - async def delete(self, key): - self.storage.pop(key, None) - return 1 - - 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 - - async def expire(self, key, time): - if key in self.storage: - return True - return False - - async def ttl(self, key): - if key in self.storage: - return 300 - return -2 - - async def ping(self): - return True - - async def sadd(self, name, *values): - if name not in self.storage: - self.storage[name] = set() - elif not isinstance(self.storage[name], set): - self.storage[name] = set(self.storage[name]) - - added = 0 - for val in values: - if val not in self.storage[name]: - self.storage[name].add(val) - added += 1 - return added - - async def srem(self, key, *values): - s = self.storage.get(key) - if not isinstance(s, set): - return 0 - before = len(s) - s.difference_update(values) - return before - len(s) - - async def smembers(self, key): - s = self.storage.get(key) - return set(s) if isinstance(s, set) else set() - - async def scard(self, key): - s = self.storage.get(key) - return len(s) if isinstance(s, set) else 0 - - @pytest.fixture() def mock_redis(monkeypatch): - fake = FakeRedis() + fake = FakeAsyncRedis(decode_responses=True) monkeypatch.setattr(security_module, "redis_client", fake) monkeypatch.setattr(health_module, "redis_client", fake) return fake diff --git a/tests/test_auth.py b/tests/test_auth.py index ca4b550..842deab 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -160,11 +160,12 @@ def test_login_rate_limit_exceeded(self, client, db_session, mock_redis): 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): + @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) @@ -176,7 +177,8 @@ def test_successful_login_resets_rate_limit(self, client, db_session, mock_redis json={"username": "admin", "password": "wrong_password"} ) - assert any("user:admin" in key or "ip:" in key for key in mock_redis.storage) + 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", @@ -184,10 +186,11 @@ def test_successful_login_resets_rate_limit(self, client, db_session, mock_redis ) assert resp.status_code == 200 - assert mock_redis.storage.get("user:admin") is None + assert await mock_redis.get("login_attempts: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 + @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", diff --git a/uv.lock b/uv.lock index 6dae379..c46cfcc 100644 --- a/uv.lock +++ b/uv.lock @@ -952,6 +952,19 @@ 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.141.1" @@ -1523,6 +1536,7 @@ dependencies = [ [package.dev-dependencies] ci = [ { name = "celery" }, + { name = "fakeredis" }, { name = "fastapi" }, { name = "httpx" }, { name = "httpx2" }, @@ -1530,12 +1544,14 @@ ci = [ { name = "prometheus-fastapi-instrumentator" }, { name = "pydantic" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "ruff" }, { name = "sqlalchemy" }, ] dev = [ { name = "celery" }, { name = "checkov" }, + { name = "fakeredis" }, { name = "fastapi" }, { name = "httpx" }, { name = "httpx2" }, @@ -1545,6 +1561,7 @@ dev = [ { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pyyaml" }, { name = "redis" }, { name = "requests" }, @@ -1575,6 +1592,7 @@ requires-dist = [ [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" }, @@ -1582,12 +1600,14 @@ ci = [ { 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 = "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" }, @@ -1597,6 +1617,7 @@ dev = [ { 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" }, @@ -2318,6 +2339,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" From 0e724aa80a48fee6b4ece0c9c5bd8a0329ac7e7a Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Wed, 9 Sep 2026 00:56:49 +0300 Subject: [PATCH 33/35] logger update --- CHANGELOG.md | 4 + dev/.env.dev.example | 8 - dev/docker-compose.dev.yml | 110 +------------ dev/env.dev.example | 19 +++ docker-compose.prod.yml | 9 +- docker/Dockerfile.celery | 53 +++++- docker/Dockerfile.fastapi | 53 +++++- docker/requirements_celery.txt | 11 -- docker/requirements_fastapi.txt | 12 -- env.example | 4 + .../templates/celery-beat-deployment.yaml | 2 + .../templates/celery-worker-deployment.yaml | 3 +- mona-chart/templates/fastapi-deployment.yaml | 2 + mona-chart/templates/postgres-deployment.yaml | 4 +- mona-chart/values.yaml | 5 + mona_core/celery_conf.py | 6 +- mona_core/config.py | 3 + mona_core/logger.py | 94 +++++++++++ mona_core/main.py | 2 + mona_core/ml.py | 12 +- mona_core/routers/dashboard.py | 1 - mona_core/routers/devices.py | 32 +++- mona_core/routers/model.py | 9 +- mona_core/routers/users.py | 55 +++++-- mona_core/schemas.py | 1 + mona_core/security.py | 13 +- mona_core/tasks.py | 155 +++++++++++++++--- pyproject.toml | 40 ++--- terraform/main.tf | 17 +- terraform/terraform.tfvars.example | 12 +- terraform/variables.tf | 12 ++ uv.lock | 83 +++++----- 32 files changed, 568 insertions(+), 278 deletions(-) delete mode 100644 dev/.env.dev.example create mode 100644 dev/env.dev.example delete mode 100644 docker/requirements_celery.txt delete mode 100644 docker/requirements_fastapi.txt create mode 100644 mona_core/logger.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c1272d8..15f1338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,15 @@ All notable changes to this project will be documented in this file. - 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. diff --git a/dev/.env.dev.example b/dev/.env.dev.example deleted file mode 100644 index 5703ce9..0000000 --- a/dev/.env.dev.example +++ /dev/null @@ -1,8 +0,0 @@ -POSTGRES_USER=myuser123 -POSTGRES_PASSWORD=changePassword -POSTGRES_DB=mydb - -ADMIN_USERNAMES=admin -ADMIN_PASSWORDS=admin123 -USER_USERNAMES=user1 -USER_PASSWORDS=userpass1 diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml index 1fa1703..4dbe5b4 100644 --- a/dev/docker-compose.dev.yml +++ b/dev/docker-compose.dev.yml @@ -1,100 +1,3 @@ -# 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 -# container_name: mona-fastapi-dev -# restart: unless-stopped -# depends_on: -# postgres: -# condition: service_healthy -# redis: -# condition: service_healthy -# env_file: -# - .env.dev -# environment: -# # POSTGRES_USER: ${POSTGRES_USER:-myuser} -# # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-1234} -# # POSTGRES_DB: ${POSTGRES_DB:-mydb} -# POSTGRES_HOST: postgres -# POSTGRES_PORT: 5432 -# REDIS_URL: redis://redis:6379 -# CORS_ORIGINS: '["http://localhost:30080","http://localhost:30081", "http://localhost:5173"]' -# 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" - -# 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" - -# 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 - services: postgres: image: postgres:16-alpine @@ -146,10 +49,11 @@ services: env_file: - .env.dev environment: + LOG_LEVEL: ${LOG_LEVEL:-DEBUG} POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 + POSTGRES_PORT: ${POSTGRES_PORT:-5432} REDIS_URL: redis://redis:6379 - CORS_ORIGINS: '["http://localhost:30080","http://localhost:30081","http://localhost:5173"]' + CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]} ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin} ADMIN_PASSWORDS: ${ADMIN_PASSWORDS:-admin123} USER_USERNAMES: ${USER_USERNAMES:-} @@ -187,11 +91,14 @@ services: env_file: - .env.dev environment: + LOG_LEVEL: ${LOG_LEVEL:-DEBUG} PYTHONPATH: /app/mona_core POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 + 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 @@ -214,9 +121,10 @@ services: env_file: - .env.dev environment: + LOG_LEVEL: ${LOG_LEVEL:-DEBUG} PYTHONPATH: /app/mona_core POSTGRES_HOST: postgres - POSTGRES_PORT: 5432 + POSTGRES_PORT: ${POSTGRES_PORT:-5432} REDIS_URL: redis://redis:6379 volumes: - ..:/app 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 index 80c291d..de3f5a4 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -51,12 +51,13 @@ services: 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: "5432" + POSTGRES_PORT: ${POSTGRES_PORT:-5432} REDIS_URL: redis://redis:6379 CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:30081"]} ADMIN_USERNAMES: ${ADMIN_USERNAMES:-admin} @@ -93,11 +94,12 @@ services: 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: "5432" + 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" @@ -127,11 +129,12 @@ services: 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: "5432" + 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: diff --git a/docker/Dockerfile.celery b/docker/Dockerfile.celery index ae44543..e44268c 100644 --- a/docker/Dockerfile.celery +++ b/docker/Dockerfile.celery @@ -1,21 +1,56 @@ +# 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 -RUN apt-get update \ - && apt-get upgrade -y \ - && rm -rf /var/lib/apt/lists/* +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 -COPY docker/requirements_celery.txt . -RUN pip install --no-cache-dir -r requirements_celery.txt +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 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 +COPY --from=builder /opt/venv /opt/venv + +ENV PATH="/opt/venv/bin:$PATH" ENV PYTHONPATH=/app/mona_core RUN chown -R app:app /app diff --git a/docker/Dockerfile.fastapi b/docker/Dockerfile.fastapi index 4bb7089..fff585b 100644 --- a/docker/Dockerfile.fastapi +++ b/docker/Dockerfile.fastapi @@ -1,16 +1,55 @@ -FROM python:3.13 AS builder +# 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 docker/requirements_fastapi.txt . -RUN pip install --no-cache-dir -r requirements_fastapi.txt +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 /usr/local /usr/local - -RUN pip install --no-cache-dir --upgrade "setuptools>=78.1.1" "msgpack>=1.2.1" +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 @@ -21,4 +60,4 @@ 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 +CMD ["uvicorn", "mona_core.main:app", "--host", "0.0.0.0", "--port", "8080"] 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 index 1daf637..44728cc 100644 --- a/env.example +++ b/env.example @@ -1,9 +1,13 @@ # 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= 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 ba68095..5d3d029 100644 --- a/mona-chart/templates/postgres-deployment.yaml +++ b/mona-chart/templates/postgres-deployment.yaml @@ -43,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 568fc3e..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 diff --git a/mona_core/celery_conf.py b/mona_core/celery_conf.py index fde398f..abf6776 100644 --- a/mona_core/celery_conf.py +++ b/mona_core/celery_conf.py @@ -11,9 +11,9 @@ backend=redis_url, ) -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 227a4e5..1868ae0 100644 --- a/mona_core/config.py +++ b/mona_core/config.py @@ -5,6 +5,9 @@ class Settings(BaseSettings): + # logger + log_level: str = "INFO" + # api cors_origins: list[str] = ["http://localhost:30081", "http://localhost:5173"] 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 41cfcfa..8ca7ca0 100644 --- a/mona_core/main.py +++ b/mona_core/main.py @@ -6,6 +6,7 @@ from prometheus_fastapi_instrumentator import Instrumentator from mona_core.config import settings +from mona_core.logger import configure_logging from mona_core.security import ( admin_router, auth_router, @@ -17,6 +18,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): seed_admin() + configure_logging() yield diff --git a/mona_core/ml.py b/mona_core/ml.py index 45b4e3e..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,6 +26,9 @@ from mona_core.config import settings from mona_core.db import Anomaly, Metric, SessionLocal, TrainedModel +# ─── Logger ───────────────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + # ─── Helpers ──────────────────────────────────────────────────────────────── @@ -144,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() @@ -156,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): @@ -189,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", @@ -197,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/dashboard.py b/mona_core/routers/dashboard.py index 844bc35..162074e 100644 --- a/mona_core/routers/dashboard.py +++ b/mona_core/routers/dashboard.py @@ -19,7 +19,6 @@ def get_dashboard_data( 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 diff --git a/mona_core/routers/devices.py b/mona_core/routers/devices.py index 7b5717d..88f245d 100644 --- a/mona_core/routers/devices.py +++ b/mona_core/routers/devices.py @@ -1,7 +1,9 @@ +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 @@ -16,6 +18,9 @@ ) from mona_core.validators import DeviceCreate +# ─── Logger ───────────────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + @user_router.get("/devices", response_model=list[DeviceOut]) def list_devices( @@ -31,22 +36,39 @@ def list_devices( "/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() - except Exception: + 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", ) - db.refresh(dev) + 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) @@ -59,12 +81,14 @@ def delete_device(device_id: int, db: Session = Depends(get_db)) -> MessageRespo db.commit() + logger.info("Device deleted", extra={"device_id": device_id}) return MessageResponse(message="Device deleted") except HTTPException: raise - except Exception as e: + except Exception: db.rollback() + logger.exception("Error during deleting device") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Database error: {str(e)}", + detail="Server error", ) diff --git a/mona_core/routers/model.py b/mona_core/routers/model.py index 57aa3eb..e88a452 100644 --- a/mona_core/routers/model.py +++ b/mona_core/routers/model.py @@ -1,3 +1,4 @@ +import logging from datetime import UTC, datetime, timedelta from fastapi import Depends, HTTPException, Query, status @@ -22,6 +23,9 @@ user_router, ) +# ─── Logger ───────────────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + @user_router.get("/anomalies", response_model=AnomalyOut) def get_anomalies( @@ -111,9 +115,10 @@ def delete_model(db: Session = Depends(get_db)) -> DeleteModelOut: message="Model deleted. Celery switched to auto-mode.", ) - except Exception as e: + 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=str(e), + detail="Server error", ) diff --git a/mona_core/routers/users.py b/mona_core/routers/users.py index 60a7aa9..d02d3ac 100644 --- a/mona_core/routers/users.py +++ b/mona_core/routers/users.py @@ -1,3 +1,5 @@ +import logging + from fastapi import Depends, HTTPException, Query, status from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError @@ -19,6 +21,9 @@ remove_all_sessions, ) +# ─── Logger ───────────────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + @admin_router.get("/users", response_model=list[UserGetOut]) def get_users( @@ -36,6 +41,7 @@ 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) @@ -44,11 +50,18 @@ def create_user( 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") @@ -71,6 +84,7 @@ async def delete_user( 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", @@ -80,15 +94,17 @@ async def delete_user( try: db.commit() - except Exception as e: + except Exception: db.rollback() + logger.exception("Server error during user deletion") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e), + detail="Server error", ) await remove_all_sessions(user_id) + logger.info("User deleted", extra={"user_id": user_id}) return MessageResponse(message="User deleted") @@ -106,20 +122,24 @@ def change_user_password( query = select(Users).where(Users.username == body.username) res = db.execute(query).scalar_one_or_none() if not res: - # log Warn + logger.warning("User not found for changing user password") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Username or Password invalid", + detail="Username invalid", ) res.set_password(body.new_password) try: db.commit() - except Exception as e: + 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=str(e), + detail="Server error", ) + logger.info("Password changed for user", extra={"username": body.username}) return MessageResponse(message="Password changed") @@ -134,27 +154,36 @@ def change_user_role( 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.role == body.role: + 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", ) - elif user.username == current_user.username: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="You cannot change your own role", - ) user.role = body.role try: @@ -162,8 +191,10 @@ def change_user_role( 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 index bbc1983..62cdecb 100644 --- a/mona_core/schemas.py +++ b/mona_core/schemas.py @@ -26,6 +26,7 @@ class UserGetOut(BaseModel): class DeviceOut(BaseModel): + id: int ip: str name: str is_active: bool = True diff --git a/mona_core/security.py b/mona_core/security.py index ae0b60c..31985f2 100644 --- a/mona_core/security.py +++ b/mona_core/security.py @@ -1,4 +1,5 @@ import json +import logging import os import secrets @@ -16,6 +17,9 @@ UserSession, ) +# ─── Logger ───────────────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + # ─── helpers ──────────────────────────────────────────────────────────────── def get_db(): @@ -68,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): @@ -96,9 +100,9 @@ def seed_admin() -> None: _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 ──────────────────────────────────────────────────────────────── @@ -114,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", diff --git a/mona_core/tasks.py b/mona_core/tasks.py index 72da0e8..a5e3019 100644 --- a/mona_core/tasks.py +++ b/mona_core/tasks.py @@ -1,7 +1,9 @@ +import logging import os from datetime import UTC, datetime, timedelta -import requests +# import requests +import httpx2 import skops.io as sio from sqlalchemy import select @@ -9,6 +11,9 @@ 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( @@ -17,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): @@ -45,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.""" @@ -73,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) + 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() - metric = Metric(cpu=cpu, ram=ram, device=job) - db.add(metric) - results.append({"device": job, "cpu": round(cpu, 2), "ram": round(ram, 2)}) + 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 - db.commit() - return results + 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() @@ -152,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/pyproject.toml b/pyproject.toml index fa3afbf..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] @@ -47,39 +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 98a7fc0..fd07fc8 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -134,6 +134,21 @@ resource "helm_release" "mona_app" { file("${path.module}/../mona-chart/values.yaml"), ] + 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" value = var.postgres_user @@ -196,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/uv.lock b/uv.lock index c46cfcc..da0e935 100644 --- a/uv.lock +++ b/uv.lock @@ -920,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" @@ -1517,115 +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" From b5c078fb9cf935a7906180c7ae7fe8c45d8091f6 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Wed, 9 Sep 2026 01:17:02 +0300 Subject: [PATCH 34/35] tests update --- tests/conftest.py | 66 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_ml.py | 2 +- tests/test_tasks.py | 35 +++++++++++------------- tests/test_users.py | 2 +- 4 files changed, 83 insertions(+), 22 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 20c45e4..dbaf3e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ 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 @@ -136,3 +137,68 @@ def mock_redis(monkeypatch): monkeypatch.setattr(security_module, "redis_client", fake) monkeypatch.setattr(health_module, "redis_client", fake) return fake + +# ─── Prometheus mocks (used by tasks._query / tasks.collect_and_save) ─────── +class FakeResponse: + """Mimics an httpx2 Response, for mocking tasks.httpx2.get() calls.""" + + def __init__(self, value=None, empty=False): + self._value = value + self._empty = empty + + def raise_for_status(self): + pass + + def json(self): + if self._empty: + return {"data": {"result": []}} + return {"data": {"result": [{"value": [123, str(self._value)]}]}} + +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, ...]. + """ + + def __init__(self, values): + self._values = iter(values) + self.calls = [] + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + return False + + def get(self, path, params=None): + self.calls.append(params) + return FakeResponse(value=next(self._values)) + + +@pytest.fixture() +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_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_tasks.py b/tests/test_tasks.py index 8d7a00e..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,7 +258,7 @@ 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): diff --git a/tests/test_users.py b/tests/test_users.py index 0d9d561..47cc9d9 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -134,7 +134,7 @@ def test_change_password_user_not_found(self, client, mock_admin_auth): json={"username": "ghost", "new_password": "newpass1"}, ) assert resp.status_code == 404 - assert resp.json()["message"] == "Username or Password invalid" + 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") From e248a438257ad4e614adea00e9beea1c14c50bf9 Mon Sep 17 00:00:00 2001 From: gwill1337 Date: Mon, 14 Sep 2026 23:34:58 +0300 Subject: [PATCH 35/35] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f1338..b76edd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # CHANGELOG All notable changes to this project will be documented in this file. -## [v0.7.0] - 2026-08-28 - Major update +## [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.