fix: address 54 MEDIUM/LOW priority PR review issues

Comprehensive fixes across Python code, shell scripts, and documentation
addressing all remaining MEDIUM and LOW priority review comments.

Python Code Fixes (27 fixes):
- tools/info.py: Simplified dispatch with lookup tables, defensive guards,
  CPU fallback formatting, !s conversion flags, module-level sync assertion
- tools/docker.py: Case-insensitive container ID regex, keyword-only confirm,
  module-level ALL_ACTIONS constant
- tools/virtualization.py: Normalized single-VM dict responses, unified
  list/details queries
- core/client.py: Fixed HTTP client singleton race condition, compound key
  substring matching for sensitive data redaction
- subscriptions/: Extracted SSL context creation to shared helper in utils.py,
  replaced deprecated ssl._create_unverified_context API
- tools/array.py: Renamed parity_history to parity_status, hoisted ALL_ACTIONS
- tools/storage.py: Fixed dict(None) risks, temperature 0 falsiness bug
- tools/notifications.py, keys.py, rclone.py: Fixed dict(None) TypeError risks
- tests/: Fixed generator type annotations, added coverage for compound keys

Shell Script Fixes (13 fixes):
- dashboard.sh: Dynamic server discovery, conditional debug output, null-safe
  jq, notification count guard order, removed unused variables
- unraid-query.sh: Proper JSON escaping via jq, --ignore-errors and --insecure
  CLI flags, TLS verification now on by default
- validate-marketplace.sh: Removed unused YELLOW variable, defensive jq,
  simplified repository URL output

Documentation Fixes (24+ fixes):
- Version consistency: Updated all references to v0.2.0 across pyproject.toml,
  plugin.json, marketplace.json, MARKETPLACE.md, __init__.py, README files
- Tool count updates: Changed all "26 tools" references to "10 tools, 90 actions"
- Markdown lint: Fixed MD022, MD031, MD047 issues across multiple files
- Research docs: Fixed auth headers, removed web artifacts, corrected stale info
- Skills docs: Fixed query examples, endpoint counts, env var references

All 227 tests pass, ruff and ty checks clean.
This commit is contained in:
Jacob Magar
2026-02-15 17:09:31 -05:00
parent 6bbe46879e
commit 37e9424a5c
58 changed files with 1333 additions and 1175 deletions

View File

@@ -27,10 +27,11 @@ This directory contains the Claude Code marketplace configuration for the Unraid
## Available Plugins ## Available Plugins
### unraid ### unraid
Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring. Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring.
**Features:** **Features:**
- 27 read-only API endpoints - 10 tools with 90 actions (queries and mutations)
- Real-time system metrics - Real-time system metrics
- Disk health and temperature monitoring - Disk health and temperature monitoring
- Docker container management - Docker container management
@@ -39,7 +40,7 @@ Query and monitor Unraid servers via GraphQL API - array status, disk health, co
- Network share information - Network share information
- Notification management - Notification management
**Version:** 1.1.0 **Version:** 0.2.0
**Category:** Infrastructure **Category:** Infrastructure
**Tags:** unraid, monitoring, homelab, graphql, docker, virtualization **Tags:** unraid, monitoring, homelab, graphql, docker, virtualization
@@ -48,7 +49,7 @@ Query and monitor Unraid servers via GraphQL API - array status, disk health, co
After installation, configure your Unraid server credentials: After installation, configure your Unraid server credentials:
```bash ```bash
export UNRAID_URL="https://your-unraid-server/graphql" export UNRAID_API_URL="https://your-unraid-server/graphql"
export UNRAID_API_KEY="your-api-key" export UNRAID_API_KEY="your-api-key"
``` ```

View File

@@ -1,7 +1,7 @@
{ {
"name": "unraid-mcp", "name": "unraid-mcp",
"description": "Comprehensive Unraid server management and monitoring tools via GraphQL API", "description": "Comprehensive Unraid server management and monitoring tools via GraphQL API",
"version": "1.0.0", "version": "0.2.0",
"owner": { "owner": {
"name": "jmagar", "name": "jmagar",
"email": "jmagar@users.noreply.github.com", "email": "jmagar@users.noreply.github.com",
@@ -12,9 +12,9 @@
"plugins": [ "plugins": [
{ {
"name": "unraid", "name": "unraid",
"source": "./skills/unraid", "source": "./",
"description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring", "description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring",
"version": "1.1.0", "version": "0.2.0",
"tags": ["unraid", "monitoring", "homelab", "graphql", "docker", "virtualization"], "tags": ["unraid", "monitoring", "homelab", "graphql", "docker", "virtualization"],
"category": "infrastructure" "category": "infrastructure"
} }

6
.gitignore vendored
View File

@@ -34,11 +34,11 @@ logs/
# Documentation and session artifacts # Documentation and session artifacts
.docs/ .docs/
.full-review/ .full-review/
docs/plans/ /docs/plans/
docs/sessions/ /docs/sessions/
# Test planning documents # Test planning documents
DESTRUCTIVE_ACTIONS.md /DESTRUCTIVE_ACTIONS.md
# Google OAuth client secrets # Google OAuth client secrets
client_secret_*.apps.googleusercontent.com.json client_secret_*.apps.googleusercontent.com.json

View File

@@ -21,23 +21,18 @@ uv sync --group dev
# Local development with uv (recommended) # Local development with uv (recommended)
uv run unraid-mcp-server uv run unraid-mcp-server
# Using development script with hot reload
./dev.sh
# Direct module execution # Direct module execution
uv run -m unraid_mcp.main uv run -m unraid_mcp.main
``` ```
### Code Quality ### Code Quality
```bash ```bash
# Format code with black # Lint and format with ruff
uv run black unraid_mcp/
# Lint with ruff
uv run ruff check unraid_mcp/ uv run ruff check unraid_mcp/
uv run ruff format unraid_mcp/
# Type checking with mypy # Type checking with ty (Astral's fast type checker)
uv run mypy unraid_mcp/ uv run ty check unraid_mcp/
# Run tests # Run tests
uv run pytest uv run pytest
@@ -49,13 +44,13 @@ uv run pytest
docker build -t unraid-mcp-server . docker build -t unraid-mcp-server .
# Run with Docker Compose # Run with Docker Compose
docker-compose up -d docker compose up -d
# View logs # View logs
docker-compose logs -f unraid-mcp docker compose logs -f unraid-mcp
# Stop service # Stop service
docker-compose down docker compose down
``` ```
### Environment Setup ### Environment Setup

View File

@@ -130,8 +130,8 @@ Before publishing to GitHub:
5. **Create Release Tag** (Optional) 5. **Create Release Tag** (Optional)
```bash ```bash
git tag -a v1.0.0 -m "Release v1.0.0" git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v1.0.0 git push origin v0.2.0
``` ```
## User Experience ## User Experience

View File

@@ -60,11 +60,9 @@ rm -rf dist/ build/ *.egg-info/
### 2. Run Quality Checks ### 2. Run Quality Checks
```bash ```bash
# Format code # Lint and format code
uv run black unraid_mcp/
# Lint code
uv run ruff check unraid_mcp/ uv run ruff check unraid_mcp/
uv run ruff format unraid_mcp/
# Type check # Type check
uv run ty check unraid_mcp/ uv run ty check unraid_mcp/
@@ -187,6 +185,8 @@ jobs:
run: uv run twine upload dist/* run: uv run twine upload dist/*
``` ```
> **Tip:** Consider using [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) instead of API token secrets. Trusted Publishing uses OpenID Connect (OIDC) to authenticate directly from GitHub Actions without storing long-lived secrets.
## Troubleshooting ## Troubleshooting
### "File already exists" Error ### "File already exists" Error

View File

@@ -1,6 +1,6 @@
# 🚀 Unraid MCP Server # 🚀 Unraid MCP Server
[![Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![FastMCP](https://img.shields.io/badge/FastMCP-2.11.2+-green.svg)](https://github.com/jlowin/fastmcp) [![FastMCP](https://img.shields.io/badge/FastMCP-2.11.2+-green.svg)](https://github.com/jlowin/fastmcp)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -8,7 +8,7 @@
## ✨ Features ## ✨ Features
- 🔧 **26 Tools**: Complete Unraid management through MCP protocol - 🔧 **10 Tools, 90 Actions**: Complete Unraid management through MCP protocol
- 🏗️ **Modular Architecture**: Clean, maintainable, and extensible codebase - 🏗️ **Modular Architecture**: Clean, maintainable, and extensible codebase
-**High Performance**: Async/concurrent operations with optimized timeouts -**High Performance**: Async/concurrent operations with optimized timeouts
- 🔄 **Real-time Data**: WebSocket subscriptions for live log streaming - 🔄 **Real-time Data**: WebSocket subscriptions for live log streaming
@@ -45,7 +45,7 @@
``` ```
This provides instant access to Unraid monitoring and management through Claude Code with: This provides instant access to Unraid monitoring and management through Claude Code with:
- 27 GraphQL API endpoints - 10 tools exposing 90 actions via the consolidated action pattern
- Real-time system metrics - Real-time system metrics
- Disk health monitoring - Disk health monitoring
- Docker and VM management - Docker and VM management
@@ -58,7 +58,7 @@ This provides instant access to Unraid monitoring and management through Claude
### Prerequisites ### Prerequisites
- Docker and Docker Compose (recommended) - Docker and Docker Compose (recommended)
- OR Python 3.10+ with [uv](https://github.com/astral-sh/uv) for development - OR Python 3.12+ with [uv](https://github.com/astral-sh/uv) for development
- Unraid server with GraphQL API enabled - Unraid server with GraphQL API enabled
### 1. Clone Repository ### 1. Clone Repository
@@ -88,7 +88,7 @@ docker compose logs -f unraid-mcp
uv sync uv sync
# Run development server # Run development server
./dev.sh uv run unraid-mcp-server
``` ```
--- ---
@@ -147,7 +147,7 @@ cp .env.example .env
# Edit .env with your settings # Edit .env with your settings
# Run development server # Run development server
./dev.sh uv run unraid-mcp-server
``` ```
--- ---
@@ -191,45 +191,22 @@ UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle
## 🛠️ Available Tools & Resources ## 🛠️ Available Tools & Resources
### System Information & Status Each tool uses a consolidated `action` parameter to expose multiple operations, reducing context window usage. Destructive actions require `confirm=True`.
- `get_system_info()` - Comprehensive system, OS, CPU, memory, hardware info
- `get_array_status()` - Storage array status, capacity, and disk details
- `get_unraid_variables()` - System variables and settings
- `get_network_config()` - Network configuration and access URLs
- `get_registration_info()` - Unraid registration details
- `get_connect_settings()` - Unraid Connect configuration
### Docker Container Management ### Tool Categories (10 Tools, 90 Actions)
- `list_docker_containers()` - List all containers with caching options
- `manage_docker_container(id, action)` - Start/stop containers (idempotent)
- `get_docker_container_details(identifier)` - Detailed container information
### Virtual Machine Management | Tool | Actions | Description |
- `list_vms()` - List all VMs and their states |------|---------|-------------|
- `manage_vm(id, action)` - VM lifecycle (start/stop/pause/resume/reboot) | **`unraid_info`** | 19 | overview, array, network, registration, connect, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config |
- `get_vm_details(identifier)` - Detailed VM information | **`unraid_array`** | 12 | start, stop, parity_start/pause/resume/cancel/history, mount_disk, unmount_disk, clear_stats, shutdown, reboot |
| **`unraid_storage`** | 6 | shares, disks, disk_details, unassigned, log_files, logs |
### Storage & File Systems | **`unraid_docker`** | 15 | list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates |
- `get_shares_info()` - User shares information | **`unraid_vm`** | 9 | list, details, start, stop, pause, resume, force_stop, reboot, reset |
- `list_physical_disks()` - Physical disk discovery | **`unraid_notifications`** | 9 | overview, list, warnings, create, archive, unread, delete, delete_archived, archive_all |
- `get_disk_details(disk_id)` - SMART data and detailed disk info | **`unraid_rclone`** | 4 | list_remotes, config_form, create_remote, delete_remote |
| **`unraid_users`** | 8 | me, list, get, add, delete, cloud, remote_access, origins |
### Monitoring & Diagnostics | **`unraid_keys`** | 5 | list, get, create, update, delete |
- `health_check()` - Comprehensive system health assessment | **`unraid_health`** | 3 | check, test_connection, diagnose |
- `get_notifications_overview()` - Notification counts by severity
- `list_notifications(type, offset, limit)` - Filtered notification listing
- `list_available_log_files()` - Available system logs
- `get_logs(path, tail_lines)` - Log file content retrieval
### Cloud Storage (RClone)
- `list_rclone_remotes()` - List configured remotes
- `get_rclone_config_form(provider)` - Configuration schemas
- `create_rclone_remote(name, type, config)` - Create new remote
- `delete_rclone_remote(name)` - Remove existing remote
### Real-time Subscriptions & Resources
- `test_subscription_query(query)` - Test GraphQL subscriptions
- `diagnose_subscriptions()` - Subscription system diagnostics
### MCP Resources (Real-time Data) ### MCP Resources (Real-time Data)
- `unraid://logs/stream` - Live log streaming from `/var/log/syslog` with WebSocket subscriptions - `unraid://logs/stream` - Live log streaming from `/var/log/syslog` with WebSocket subscriptions
@@ -257,26 +234,27 @@ unraid-mcp/
│ │ ├── manager.py # WebSocket management │ │ ├── manager.py # WebSocket management
│ │ ├── resources.py # MCP resources │ │ ├── resources.py # MCP resources
│ │ └── diagnostics.py # Diagnostic tools │ │ └── diagnostics.py # Diagnostic tools
│ ├── tools/ # MCP tool categories │ ├── tools/ # MCP tool categories (10 tools, 90 actions)
│ │ ├── docker.py # Container management │ │ ├── info.py # System information (19 actions)
│ │ ├── system.py # System information │ │ ├── array.py # Array management (12 actions)
│ │ ├── storage.py # Storage & monitoring │ │ ├── storage.py # Storage & monitoring (6 actions)
│ │ ├── health.py # Health checks │ │ ├── docker.py # Container management (15 actions)
│ │ ├── virtualization.py # VM management │ │ ├── virtualization.py # VM management (9 actions)
│ │ ── rclone.py # Cloud storage │ │ ── notifications.py # Notification management (9 actions)
│ │ ├── rclone.py # Cloud storage (4 actions)
│ │ ├── users.py # User management (8 actions)
│ │ ├── keys.py # API key management (5 actions)
│ │ └── health.py # Health checks (3 actions)
│ └── server.py # FastMCP server setup │ └── server.py # FastMCP server setup
├── logs/ # Log files (auto-created) ├── logs/ # Log files (auto-created)
├── dev.sh # Development script
└── docker-compose.yml # Docker Compose deployment └── docker-compose.yml # Docker Compose deployment
``` ```
### Code Quality Commands ### Code Quality Commands
```bash ```bash
# Format code # Lint and format code
uv run black unraid_mcp/
# Lint code
uv run ruff check unraid_mcp/ uv run ruff check unraid_mcp/
uv run ruff format unraid_mcp/
# Type checking # Type checking
uv run ty check unraid_mcp/ uv run ty check unraid_mcp/
@@ -287,11 +265,11 @@ uv run pytest
### Development Workflow ### Development Workflow
```bash ```bash
# Start development server (kills existing processes safely) # Start development server
./dev.sh uv run unraid-mcp-server
# Stop server only # Or run via module directly
./dev.sh --kill uv run -m unraid_mcp.main
``` ```
--- ---
@@ -324,7 +302,8 @@ uv run pytest
**🔥 Port Already in Use** **🔥 Port Already in Use**
```bash ```bash
./dev.sh # Automatically kills existing processes # Kill existing process on port 6970, then restart
lsof -ti :6970 | xargs kill -9 2>/dev/null; uv run unraid-mcp-server
``` ```
**🔧 Connection Refused** **🔧 Connection Refused**

View File

@@ -1,14 +1,14 @@
# Competitive Analysis: Unraid Integration Projects # Competitive Analysis: Unraid Integration Projects
> **Date:** 2026-02-07 > **Date:** 2026-02-07
> **Purpose:** Identify features and capabilities that competing Unraid integration projects offer that our `unraid-mcp` server (26 tools, GraphQL-based) currently lacks. > **Purpose:** Identify features and capabilities that competing Unraid integration projects offer that our `unraid-mcp` server (10 tools, 90 actions, GraphQL-based) currently lacks.
## Table of Contents ## Table of Contents
- [Executive Summary](#executive-summary) - [Executive Summary](#executive-summary)
- [Project Profiles](#project-profiles) - [Project Profiles](#project-profiles)
- [1. unraid-management-agent (Go plugin)](#1-unraid-management-agent) - [1. unraid-management-agent (Go plugin)](#1-unraid-management-agent)
- [2. domalab/unraid-api-client (Python library)](#2-domalabu nraid-api-client) - [2. domalab/unraid-api-client (Python library)](#2-domalabunraid-api-client)
- [3. mcp-ssh-sre / unraid-ssh-mcp (SSH-based MCP)](#3-mcp-ssh-sre--unraid-ssh-mcp) - [3. mcp-ssh-sre / unraid-ssh-mcp (SSH-based MCP)](#3-mcp-ssh-sre--unraid-ssh-mcp)
- [4. PSUnraid (PowerShell module)](#4-psunraid) - [4. PSUnraid (PowerShell module)](#4-psunraid)
- [5. ha-unraid (Home Assistant integration)](#5-ha-unraid) - [5. ha-unraid (Home Assistant integration)](#5-ha-unraid)
@@ -22,7 +22,7 @@
## Executive Summary ## Executive Summary
Our `unraid-mcp` server provides 26 MCP tools built on the official Unraid GraphQL API. After analyzing six competing projects, we identified several significant gaps: Our `unraid-mcp` server provides 10 MCP tools (90 actions) built on the official Unraid GraphQL API. After analyzing six competing projects, we identified several significant gaps:
**Critical gaps (high-value features we lack):** **Critical gaps (high-value features we lack):**
1. **Array control operations** (start/stop array, parity check control, disk spin up/down) 1. **Array control operations** (start/stop array, parity check control, disk spin up/down)
@@ -71,7 +71,7 @@ Our `unraid-mcp` server provides 26 MCP tools built on the official Unraid Graph
**Key differentiators from our project:** **Key differentiators from our project:**
- Runs as an Unraid plugin directly on the server (no external dependency on GraphQL API) - Runs as an Unraid plugin directly on the server (no external dependency on GraphQL API)
- Collects data directly from /proc, /sys, Docker SDK, and libvirt - Collects data directly from /proc, /sys, Docker SDK, and libvirt
- 59 REST endpoints vs our 26 MCP tools - 59 REST endpoints vs our 10 MCP tools (90 actions)
- 54 MCP tools with Resources and Prompts - 54 MCP tools with Resources and Prompts
- Real-time WebSocket event streaming (9 event types, 5-60s intervals) - Real-time WebSocket event streaming (9 event types, 5-60s intervals)
- 41 Prometheus metrics for Grafana dashboards - 41 Prometheus metrics for Grafana dashboards
@@ -341,7 +341,7 @@ The project's documentation explicitly compares SSH vs API capabilities:
### Monitoring Features ### Monitoring Features
| Feature | Our MCP (26 tools) | mgmt-agent (54 MCP tools) | unraid-api-client | mcp-ssh-sre (79 actions) | PSUnraid | ha-unraid | chris-mc1 | | Feature | Our MCP (10 tools, 90 actions) | mgmt-agent (54 MCP tools) | unraid-api-client | mcp-ssh-sre (79 actions) | PSUnraid | ha-unraid | chris-mc1 |
|---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| System info (hostname, uptime) | Y | Y | Y | Y | Y | Y | N | | System info (hostname, uptime) | Y | Y | Y | Y | Y | Y | N |
| CPU usage | Y | Y | Y | Y | Y | Y | Y | | CPU usage | Y | Y | Y | Y | Y | Y | Y |
@@ -458,7 +458,7 @@ The project's documentation explicitly compares SSH vs API capabilities:
| Feature | Our MCP | mgmt-agent | unraid-api-client | mcp-ssh-sre | PSUnraid | ha-unraid | chris-mc1 | | Feature | Our MCP | mgmt-agent | unraid-api-client | mcp-ssh-sre | PSUnraid | ha-unraid | chris-mc1 |
|---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| MCP tools | Y (26) | Y (54) | N | Y (79 actions) | N | N | N | | MCP tools | Y (10 tools, 90 actions) | Y (54) | N | Y (79 actions) | N | N | N |
| MCP Resources | N | Y (5) | N | N | N | N | N | | MCP Resources | N | Y (5) | N | N | N | N | N |
| MCP Prompts | N | Y (3) | N | N | N | N | N | | MCP Prompts | N | Y (3) | N | N | N | N | N |
| REST API | N | Y (59) | N | N | N | N | N | | REST API | N | Y (59) | N | N | N | N | N |

View File

@@ -1,7 +1,7 @@
# Unraid API Feature Gap Analysis # Unraid API Feature Gap Analysis
> **Date:** 2026-02-07 > **Date:** 2026-02-07
> **Purpose:** Comprehensive inventory of every API capability that could become an MCP tool, cross-referenced against our current 26 tools to identify gaps. > **Purpose:** Comprehensive inventory of every API capability that could become an MCP tool, cross-referenced against our current 10 tools (90 actions) to identify gaps.
> **Sources:** 7 research documents (3,800+ lines), Unraid API source code analysis, community project reviews, official documentation crawl. > **Sources:** 7 research documents (3,800+ lines), Unraid API source code analysis, community project reviews, official documentation crawl.
--- ---
@@ -497,7 +497,7 @@ GRAPHQL_PUBSUB_CHANNEL {
## F. API Capabilities NOT Currently in the MCP Server ## F. API Capabilities NOT Currently in the MCP Server
The current MCP server has 26 tools. The following capabilities are available in the Unraid API but NOT covered by any existing tool. The current MCP server has 10 tools (90 actions) after consolidation. The following capabilities are available in the Unraid API but NOT covered by any existing tool.
### F.1 HIGH PRIORITY - New Tool Candidates ### F.1 HIGH PRIORITY - New Tool Candidates
@@ -663,7 +663,7 @@ The current MCP server has 26 tools. The following capabilities are available in
| **Queries** | ~30+ | 14 | ~16+ uncovered | | **Queries** | ~30+ | 14 | ~16+ uncovered |
| **Mutations** | ~50+ | 10 (start/stop Docker+VM, RClone CRUD) | ~40+ uncovered | | **Mutations** | ~50+ | 10 (start/stop Docker+VM, RClone CRUD) | ~40+ uncovered |
| **Subscriptions** | ~30+ | 0 (2 diagnostic only) | ~30+ uncovered | | **Subscriptions** | ~30+ | 0 (2 diagnostic only) | ~30+ uncovered |
| **Total Operations** | ~110+ | 24 active | ~86+ uncovered | | **Total Operations** | ~110+ | 90 active (10 tools) | ~20+ uncovered |
**Current coverage: approximately 22% of available API operations.** **Current coverage: approximately 22% of available API operations.**
@@ -691,7 +691,7 @@ Capabilities this project offers that we do NOT:
| **Update Status** | Check for OS/plugin updates | NOT available via GraphQL API | | **Update Status** | Check for OS/plugin updates | NOT available via GraphQL API |
| **Mover Control** | Invoke the mover tool | NOT available via GraphQL API (Issue #1873) | | **Mover Control** | Invoke the mover tool | NOT available via GraphQL API (Issue #1873) |
| **Disk Thresholds** | Warning/critical temp settings | Partially available via `ArrayDisk.warning`/`critical` | | **Disk Thresholds** | Warning/critical temp settings | Partially available via `ArrayDisk.warning`/`critical` |
| **54 MCP Tools** | Full MCP tool suite | We have 26 | | **54 MCP Tools** | Full MCP tool suite | We have 10 tools (90 actions) |
| **WebSocket Events** | Real-time event stream | We have diagnostic-only subscriptions | | **WebSocket Events** | Real-time event stream | We have diagnostic-only subscriptions |
### G.2 PSUnraid (PowerShell Module) ### G.2 PSUnraid (PowerShell Module)
@@ -828,7 +828,7 @@ Chose SSH over GraphQL API due to these gaps:
| | **Low Priority Subtotal** | **18** | **92** | | | **Low Priority Subtotal** | **18** | **92** |
| | **GRAND TOTAL NEW TOOLS** | **66** | **92** | | | **GRAND TOTAL NEW TOOLS** | **66** | **92** |
**Current tools: 26 | Potential total: 92 | Gap: 66 tools (72% of potential uncovered)** **Current tools: 10 (90 actions) | Potential total: ~110+ operations | Remaining gap: ~20+ uncovered operations**
--- ---

View File

@@ -1,6 +1,7 @@
[Skip to main content](https://docs.unraid.net/unraid-connect/remote-access#__docusaurus_skipToContent_fallback) # Remote Access (Unraid Connect)
On this page > **Source:** [Unraid Documentation - Remote Access](https://docs.unraid.net/unraid-connect/remote-access)
> **Scraped:** 2026-02-07 | Raw content for reference purposes
Unlock secure, browser-based access to your Unraid WebGUI from anywhere with remote access. This feature is ideal for managing your server when you're away from home - no complicated networking or VPN Tunnel setup is required. For more advanced needs, such as connecting to Docker containers or accessing network drives, a VPN Tunnel remains the recommended solution. Unlock secure, browser-based access to your Unraid WebGUI from anywhere with remote access. This feature is ideal for managing your server when you're away from home - no complicated networking or VPN Tunnel setup is required. For more advanced needs, such as connecting to Docker containers or accessing network drives, a VPN Tunnel remains the recommended solution.

View File

@@ -1,6 +1,7 @@
[Skip to main content](https://docs.unraid.net/unraid-os/release-notes/7.0.0#__docusaurus_skipToContent_fallback) # Unraid OS 7.0.0 Release Notes
On this page > **Source:** [Unraid OS Release Notes - 7.0.0](https://docs.unraid.net/unraid-os/release-notes/7.0.0)
> **Scraped:** 2026-02-07 | Raw content for reference purposes
This version of Unraid OS includes significant improvements across all subsystems, while attempting to maintain backward compatibility as much as possible. This version of Unraid OS includes significant improvements across all subsystems, while attempting to maintain backward compatibility as much as possible.
@@ -883,76 +884,3 @@ With the [Unraid Patch plugin](https://forums.unraid.net/topic/185560-unraid-pat
* Include installed patches in diagnostics * Include installed patches in diagnostics
Note: if you have the Mover Tuning plugin installed, you will be prompted to reboot in order to apply these patches. Note: if you have the Mover Tuning plugin installed, you will be prompted to reboot in order to apply these patches.
* [Upgrading](https://docs.unraid.net/unraid-os/release-notes/7.0.0#upgrading)
* [Known issues](https://docs.unraid.net/unraid-os/release-notes/7.0.0#known-issues)
* [Rolling back](https://docs.unraid.net/unraid-os/release-notes/7.0.0#rolling-back)
* [Storage](https://docs.unraid.net/unraid-os/release-notes/7.0.0#storage)
* [unRAID array optional](https://docs.unraid.net/unraid-os/release-notes/7.0.0#unraid-array-optional)
* [Share secondary storage may be assigned to a pool](https://docs.unraid.net/unraid-os/release-notes/7.0.0#share-secondary-storage-may-be-assigned-to-a-pool)
* [ReiserFS file system option has been disabled](https://docs.unraid.net/unraid-os/release-notes/7.0.0#reiserfs-file-system-option-has-been-disabled)
* [Using 'mover' to empty an array disk](https://docs.unraid.net/unraid-os/release-notes/7.0.0#using-mover-to-empty-an-array-disk)
* [Predefined shares handling](https://docs.unraid.net/unraid-os/release-notes/7.0.0#predefined-shares-handling)
* [ZFS implementation](https://docs.unraid.net/unraid-os/release-notes/7.0.0#zfs-implementation)
* [Allocation profiles for btrfs, zfs, and zfs subpools](https://docs.unraid.net/unraid-os/release-notes/7.0.0#allocation-profiles-for-btrfs-zfs-and-zfs-subpools)
* [Pool considerations](https://docs.unraid.net/unraid-os/release-notes/7.0.0#pool-considerations)
* [Other features](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-features)
* [VM Manager](https://docs.unraid.net/unraid-os/release-notes/7.0.0#vm-manager)
* [Improvements](https://docs.unraid.net/unraid-os/release-notes/7.0.0#improvements)
* [Other changes](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-changes)
* [Docker](https://docs.unraid.net/unraid-os/release-notes/7.0.0#docker)
* [Docker fork bomb prevention](https://docs.unraid.net/unraid-os/release-notes/7.0.0#docker-fork-bomb-prevention)
* [Add support for overlay2 storage driver](https://docs.unraid.net/unraid-os/release-notes/7.0.0#add-support-for-overlay2-storage-driver)
* [Other changes](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-changes-1)
* [Networking](https://docs.unraid.net/unraid-os/release-notes/7.0.0#networking)
* [Tailscale integration](https://docs.unraid.net/unraid-os/release-notes/7.0.0#tailscale-integration)
* [Support iframing the webGUI](https://docs.unraid.net/unraid-os/release-notes/7.0.0#support-iframing-the-webgui)
* [Other changes](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-changes-2)
* [webGUI](https://docs.unraid.net/unraid-os/release-notes/7.0.0#webgui)
* [Integrated Dynamix File Manager plugin](https://docs.unraid.net/unraid-os/release-notes/7.0.0#integrated-dynamix-file-manager-plugin)
* [Integrated GUI Search plugin](https://docs.unraid.net/unraid-os/release-notes/7.0.0#integrated-gui-search-plugin)
* [Outgoing Proxy Manager](https://docs.unraid.net/unraid-os/release-notes/7.0.0#outgoing-proxy-manager)
* [Notification Agents](https://docs.unraid.net/unraid-os/release-notes/7.0.0#notification-agents)
* [NTP Configuration](https://docs.unraid.net/unraid-os/release-notes/7.0.0#ntp-configuration)
* [NFS Shares](https://docs.unraid.net/unraid-os/release-notes/7.0.0#nfs-shares)
* [Dashboard](https://docs.unraid.net/unraid-os/release-notes/7.0.0#dashboard)
* [SMART improvements](https://docs.unraid.net/unraid-os/release-notes/7.0.0#smart-improvements)
* [Diagnostics](https://docs.unraid.net/unraid-os/release-notes/7.0.0#diagnostics)
* [Other changes](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-changes-3)
* [Misc](https://docs.unraid.net/unraid-os/release-notes/7.0.0#misc)
* [Other changes](https://docs.unraid.net/unraid-os/release-notes/7.0.0#other-changes-4)
* [Linux kernel](https://docs.unraid.net/unraid-os/release-notes/7.0.0#linux-kernel)
* [Base distro](https://docs.unraid.net/unraid-os/release-notes/7.0.0#base-distro)
* [Patches](https://docs.unraid.net/unraid-os/release-notes/7.0.0#patches)

View File

@@ -468,6 +468,9 @@ unraid-api apikey --delete --name "workflow key" --json
#!/bin/bash #!/bin/bash
set -e set -e
# Set up cleanup trap early so it fires even if key creation fails
cleanup() { echo "Cleaning up..."; unraid-api apikey --delete --name "temp deployment key" 2>/dev/null || true; }
# 1. Create temporary API key # 1. Create temporary API key
echo "Creating temporary API key..." echo "Creating temporary API key..."
KEY_DATA=$(unraid-api apikey --create \ KEY_DATA=$(unraid-api apikey --create \
@@ -476,20 +479,20 @@ KEY_DATA=$(unraid-api apikey --create \
--description "Temporary key for deployment $(date)" \ --description "Temporary key for deployment $(date)" \
--json) --json)
# Register trap after key creation succeeds
trap cleanup EXIT
# 2. Extract the API key # 2. Extract the API key
API_KEY=$(echo "$KEY_DATA" | jq -r '.key') API_KEY=$(echo "$KEY_DATA" | jq -r '.key')
echo "API key created successfully" echo "API key created successfully"
# 3. Use the key for operations # 3. Use the key for operations
echo "Configuring services..." echo "Configuring services..."
curl -H "Authorization: Bearer $API_KEY" \ curl -H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"provider": "azure", "clientId": "your-client-id"}' \ -d '{"provider": "azure", "clientId": "your-client-id"}' \
http://localhost:3001/graphql http://localhost:3001/graphql
# 4. Clean up (always runs, even on error)
trap 'echo "Cleaning up..."; unraid-api apikey --delete --name "temp deployment key"' EXIT
echo "Deployment completed successfully" echo "Deployment completed successfully"
``` ```
@@ -714,6 +717,8 @@ type Mutation {
} }
``` ```
> **Note:** The client schema above uses `ID!` for disk mutation args (e.g., `mountArrayDisk(id: ID!)`), but the actual server resolvers use `PrefixedID!`. The MCP tool code correctly uses `PrefixedID!` based on server source analysis.
#### Subscription Type #### Subscription Type
```graphql ```graphql

View File

@@ -119,7 +119,7 @@ The API implements a **multi-layered security architecture** separating authenti
Applications can request API access via a self-service flow ([source](https://docs.unraid.net/API/api-key-app-developer-authorization-flow/)): Applications can request API access via a self-service flow ([source](https://docs.unraid.net/API/api-key-app-developer-authorization-flow/)):
``` ```
https://[unraid-server]/ApiKeyAuthorize?name=MyApp&&scopes=docker:read,vm:*&&redirect_uri=https://myapp.com/callback&&state=abc123 https://[unraid-server]/ApiKeyAuthorize?name=MyApp&scopes=docker:read,vm:*&redirect_uri=https://myapp.com/callback&state=abc123
``` ```
**Scope Format:** `resource:action` pattern **Scope Format:** `resource:action` pattern
@@ -302,7 +302,7 @@ The subscription system is event-driven: file changes on disk (detected by choki
**jmagar/unraid-mcp** (this project) is the primary MCP server for Unraid ([source](https://glama.ai/mcp/servers/@jmagar/unraid-mcp), [source](https://mcpmarket.com/server/unraid)): **jmagar/unraid-mcp** (this project) is the primary MCP server for Unraid ([source](https://glama.ai/mcp/servers/@jmagar/unraid-mcp), [source](https://mcpmarket.com/server/unraid)):
- Python-based MCP server using FastMCP framework - Python-based MCP server using FastMCP framework
- 26 tools for comprehensive Unraid management - 10 tools with 90 actions for comprehensive Unraid management
- Read-only access by default for safety - Read-only access by default for safety
- Listed on Glama, MCP Market, MCPServers.com, LangDB, UBOS, JuheAPI - Listed on Glama, MCP Market, MCPServers.com, LangDB, UBOS, JuheAPI
- 21 GitHub stars - 21 GitHub stars

View File

@@ -322,6 +322,7 @@ query {
``` ```
#### RClone Remotes (`rclone`) #### RClone Remotes (`rclone`)
```graphql ```graphql
query { query {
rclone { rclone {
@@ -334,6 +335,7 @@ query {
``` ```
#### Notifications #### Notifications
```graphql ```graphql
query { query {
notifications { notifications {
@@ -343,6 +345,7 @@ query {
``` ```
#### Shares #### Shares
```graphql ```graphql
query { query {
shares { shares {
@@ -354,6 +357,7 @@ query {
### Mutation Types ### Mutation Types
#### Docker Container Management #### Docker Container Management
```graphql ```graphql
mutation { mutation {
docker { docker {
@@ -366,6 +370,7 @@ mutation {
- Mutations are idempotent (starting an already-running container returns success) - Mutations are idempotent (starting an already-running container returns success)
#### VM Management #### VM Management
```graphql ```graphql
mutation { mutation {
vm { vm {

View File

@@ -14,7 +14,7 @@ version = "0.2.0"
description = "MCP Server for Unraid API - provides tools to interact with an Unraid server's GraphQL API" description = "MCP Server for Unraid API - provides tools to interact with an Unraid server's GraphQL API"
readme = "README.md" readme = "README.md"
license = {file = "LICENSE"} license = {file = "LICENSE"}
requires-python = ">=3.10" requires-python = ">=3.12"
authors = [ authors = [
{name = "jmagar", email = "jmagar@users.noreply.github.com"} {name = "jmagar", email = "jmagar@users.noreply.github.com"}
] ]
@@ -46,8 +46,6 @@ classifiers = [
# Python Versions # Python Versions
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
@@ -82,44 +80,6 @@ dependencies = [
"pytz>=2025.2", "pytz>=2025.2",
] ]
[project.optional-dependencies]
# Development dependencies
dev = [
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"pytest-cov>=7.0.0",
"types-pytz>=2025.2.0.20250809",
"ty>=0.0.15",
"ruff>=0.12.8",
"black>=25.1.0",
"build>=1.2.2",
"twine>=6.0.1",
]
# Testing only
test = [
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"pytest-cov>=7.0.0",
]
# Linting and formatting only
lint = [
"ruff>=0.12.8",
"black>=25.1.0",
"ty>=0.0.15",
]
# Type checking stubs
types = [
"types-pytz>=2025.2.0.20250809",
]
# All dev dependencies
all = [
"unraid-mcp[dev,test,lint,types]",
]
# ============================================================================ # ============================================================================
# Project URLs # Project URLs
# ============================================================================ # ============================================================================
@@ -142,7 +102,6 @@ unraid-mcp = "unraid_mcp.main:main"
# Build Configuration # Build Configuration
# ============================================================================ # ============================================================================
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["unraid_mcp"]
only-include = ["unraid_mcp"] only-include = ["unraid_mcp"]
[tool.hatch.build.targets.sdist] [tool.hatch.build.targets.sdist]
@@ -167,33 +126,10 @@ exclude = [
] ]
# ============================================================================ # ============================================================================
# Tool Configuration: Black (Code Formatting) # Tool Configuration: Ruff (Linting & Formatting)
# ============================================================================
[tool.black]
line-length = 100
target-version = ['py310', 'py311', 'py312', 'py313']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.ty_cache
| \.tox
| \.venv
| \.cache
| build
| dist
| __pycache__
)/
'''
# ============================================================================
# Tool Configuration: Ruff (Linting)
# ============================================================================ # ============================================================================
[tool.ruff] [tool.ruff]
target-version = "py310" target-version = "py312"
line-length = 100 line-length = 100
cache-dir = ".cache/.ruff_cache" cache-dir = ".cache/.ruff_cache"
@@ -236,7 +172,7 @@ select = [
"RUF", "RUF",
] ]
ignore = [ ignore = [
"E501", # line too long (handled by black) "E501", # line too long (handled by ruff formatter)
"B008", # function calls in argument defaults "B008", # function calls in argument defaults
"C901", # too complex "C901", # too complex
"D100", # missing docstring in public module "D100", # missing docstring in public module
@@ -266,7 +202,7 @@ lines-after-imports = 2
# Tool Configuration: ty (Type Checking) # Tool Configuration: ty (Type Checking)
# ============================================================================ # ============================================================================
[tool.ty.environment] [tool.ty.environment]
python-version = "3.10" python-version = "3.12"
[tool.ty.analysis] [tool.ty.analysis]
respect-type-ignore-comments = true respect-type-ignore-comments = true
@@ -351,7 +287,6 @@ dev = [
"types-pytz>=2025.2.0.20250809", "types-pytz>=2025.2.0.20250809",
"ty>=0.0.15", "ty>=0.0.15",
"ruff>=0.12.8", "ruff>=0.12.8",
"black>=25.1.0",
"build>=1.2.2", "build>=1.2.2",
"twine>=6.0.1", "twine>=6.0.1",
] ]

View File

@@ -6,7 +6,6 @@ set -euo pipefail
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Counters # Counters
@@ -62,8 +61,14 @@ check "Marketplace has repository" "jq -e '.repository' .claude-plugin/marketpla
check "Marketplace has owner" "jq -e '.owner' .claude-plugin/marketplace.json" check "Marketplace has owner" "jq -e '.owner' .claude-plugin/marketplace.json"
# Verify source path # Verify source path
PLUGIN_SOURCE=$(jq -r '.plugins[] | select(.name == "unraid") | .source' .claude-plugin/marketplace.json) PLUGIN_SOURCE=$(jq -r '.plugins[]? | select(.name == "unraid") | .source // empty' .claude-plugin/marketplace.json 2>/dev/null || true)
check "Plugin source path is valid" "test -d \"$PLUGIN_SOURCE\"" if [ -n "$PLUGIN_SOURCE" ]; then
check "Plugin source path is valid" "test -d \"$PLUGIN_SOURCE\""
else
CHECKS=$((CHECKS + 1))
FAILED=$((FAILED + 1))
echo -e "Checking: Plugin source path is valid... ${RED}${NC} (plugin not found in marketplace)"
fi
echo "" echo ""
echo "=== Results ===" echo "=== Results ==="
@@ -76,5 +81,5 @@ else
echo -e "${GREEN}All checks passed!${NC}" echo -e "${GREEN}All checks passed!${NC}"
echo "" echo ""
echo "Marketplace is ready for distribution at:" echo "Marketplace is ready for distribution at:"
echo " https://github.com/$(jq -r '.repository' .claude-plugin/marketplace.json | sed 's|https://github.com/||')" echo " $(jq -r '.repository' .claude-plugin/marketplace.json)"
fi fi

View File

@@ -1,7 +1,7 @@
{ {
"name": "unraid", "name": "unraid",
"description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring", "description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring",
"version": "1.1.0", "version": "0.2.0",
"author": { "author": {
"name": "jmagar", "name": "jmagar",
"email": "jmagar@users.noreply.github.com" "email": "jmagar@users.noreply.github.com"
@@ -14,7 +14,7 @@
"args": [ "args": [
"run", "run",
"--directory", "--directory",
"${CLAUDE_PLUGIN_ROOT}/../..", "${CLAUDE_PLUGIN_ROOT}",
"unraid-mcp-server" "unraid-mcp-server"
], ],
"env": { "env": {

View File

@@ -143,7 +143,7 @@ This skill activates when you mention:
## Version ## Version
- **Skill Version:** 1.0.0 - **Skill Version:** 0.2.0
- **API Version:** Unraid 7.2 GraphQL - **API Version:** Unraid 7.2 GraphQL
- **Tested:** 2026-01-21 - **Tested:** 2026-01-21
- **Endpoints:** 27 working read-only queries - **Endpoints:** 27 working read-only queries

View File

@@ -123,7 +123,7 @@ Always use `metrics` for monitoring, `info` for specifications.
**List VMs:** **List VMs:**
```bash ```bash
./scripts/unraid-query.sh -q "{ vms { name state cpus memory } } }" ./scripts/unraid-query.sh -q "{ vms { domain { name state } } }"
``` ```
**Note:** Container output logs are NOT accessible via API. Use `docker logs` via SSH. **Note:** Container output logs are NOT accessible via API. Use `docker logs` via SSH.

View File

@@ -917,7 +917,7 @@ Common differences from online documentation:
2. **Use `array.disks` for array disks** - The top-level `disks` query includes ALL disks (USB, SSDs, etc.) 2. **Use `array.disks` for array disks** - The top-level `disks` query includes ALL disks (USB, SSDs, etc.)
3. **Always check errors** - GraphQL returns errors in `errors` array 3. **Always check errors** - GraphQL returns errors in `errors` array
4. **Use introspection** - Field names can vary between versions 4. **Use introspection** - Field names can vary between versions
5. **Sizes are in kilobytes** - Disk sizes and capacities are in KB, not bytes 5. **Sizes vary by context** - Disk/array capacities are in kilobytes; memory values (from `info.memory`) are in bytes
6. **Temperature is Celsius** - All temperature values are in Celsius 6. **Temperature is Celsius** - All temperature values are in Celsius
7. **Handle empty arrays** - Many queries return `[]` when no data exists 7. **Handle empty arrays** - Many queries return `[]` when no data exists
8. **Use viewer role** - Create API keys with "Viewer" role for read-only access 8. **Use viewer role** - Create API keys with "Viewer" role for read-only access
@@ -926,8 +926,8 @@ Common differences from online documentation:
## 🚫 Known Limitations ## 🚫 Known Limitations
1. **No Docker container logs** - Container output logs are NOT accessible via API 1. **No Docker container logs** - Container output logs are NOT accessible via the read-only query API (use `docker.logs` mutation)
2. **No real-time streaming** - All queries are request/response, no WebSocket subscriptions 2. **WebSocket subscriptions are available** - The Unraid API supports real-time subscriptions (array, Docker stats, notifications, etc.) via WebSocket
3. **Some queries require higher permissions** - Read-only "Viewer" role may not access all queries 3. **Some queries require higher permissions** - Read-only "Viewer" role may not access all queries
4. **No mutation examples included** - This guide covers read-only queries only 4. **No mutation examples included** - This guide covers read-only queries only

View File

@@ -32,7 +32,7 @@ Complete list of available GraphQL read-only endpoints in Unraid 7.2+.
19. **`isSSOEnabled`** - SSO status 19. **`isSSOEnabled`** - SSO status
20. **`oidcProviders`** - OIDC providers 20. **`oidcProviders`** - OIDC providers
## API Management (2) ## API Management (1)
21. **`apiKeys`** - List API keys 21. **`apiKeys`** - List API keys
## Customization (3) ## Customization (3)

View File

@@ -10,7 +10,7 @@ export UNRAID_URL="https://your-unraid-server/graphql"
export UNRAID_API_KEY="your-api-key-here" export UNRAID_API_KEY="your-api-key-here"
# Or use the helper script directly # Or use the helper script directly
./scripts/unraid-query.sh -u "$UNRAID_URL" -k "$API_KEY" -q "{ online }" ./scripts/unraid-query.sh -u "$UNRAID_URL" -k "$UNRAID_API_KEY" -q "{ online }"
``` ```
## Common Queries ## Common Queries

View File

@@ -3,7 +3,9 @@
Common issues and solutions when working with the Unraid GraphQL API. Common issues and solutions when working with the Unraid GraphQL API.
## "Cannot query field" error ## "Cannot query field" error
Field name doesn't exist in your Unraid version. Use introspection to find valid fields: Field name doesn't exist in your Unraid version. Use introspection to find valid fields:
```bash ```bash
./scripts/unraid-query.sh -q "{ __type(name: \"TypeName\") { fields { name } } }" ./scripts/unraid-query.sh -q "{ __type(name: \"TypeName\") { fields { name } } }"
``` ```

View File

@@ -13,10 +13,23 @@ OUTPUT_FILE="$HOME/memory/bank/unraid-inventory.md"
# Load credentials from .env for all servers # Load credentials from .env for all servers
load_env_file || exit 1 load_env_file || exit 1
for server in "TOOTIE" "SHART"; do
# Discover configured servers dynamically from UNRAID_<NAME>_URL env vars
SERVERS=()
while IFS='=' read -r var_name _; do
if [[ "$var_name" =~ ^UNRAID_(.+)_URL$ ]]; then
SERVERS+=("${BASH_REMATCH[1]}")
fi
done < <(env)
if [ ${#SERVERS[@]} -eq 0 ]; then
echo "Error: No servers found. Set UNRAID_<NAME>_URL and UNRAID_<NAME>_API_KEY env vars."
exit 1
fi
for server in "${SERVERS[@]}"; do
url_var="UNRAID_${server}_URL" url_var="UNRAID_${server}_URL"
key_var="UNRAID_${server}_API_KEY" key_var="UNRAID_${server}_API_KEY"
name_var="UNRAID_${server}_NAME"
validate_env_vars "$url_var" "$key_var" || exit 1 validate_env_vars "$url_var" "$key_var" || exit 1
done done
@@ -36,9 +49,10 @@ process_server() {
echo "Querying server: $NAME..." echo "Querying server: $NAME..."
export UNRAID_URL="$URL" UNRAID_URL="$URL"
export UNRAID_API_KEY="$API_KEY" UNRAID_API_KEY="$API_KEY"
export IGNORE_ERRORS="true" IGNORE_ERRORS="true"
export UNRAID_URL UNRAID_API_KEY IGNORE_ERRORS
QUERY='query Dashboard { QUERY='query Dashboard {
info { info {
@@ -73,13 +87,16 @@ process_server() {
RESPONSE=$("$QUERY_SCRIPT" -q "$QUERY" -f json) RESPONSE=$("$QUERY_SCRIPT" -q "$QUERY" -f json)
# Debug output # Debug output (only when DEBUG is set)
echo "$RESPONSE" > "${NAME}_debug.json" if [ "${DEBUG:-}" = "true" ]; then
echo "$RESPONSE" > "/tmp/${NAME}_debug.json"
fi
# Check if response is valid JSON # Check if response is valid JSON
if ! echo "$RESPONSE" | jq -e . >/dev/null 2>&1; then if ! echo "$RESPONSE" | jq -e . >/dev/null 2>&1; then
echo "Error querying $NAME: Invalid response" echo "Error querying $NAME: Invalid response"
echo "Response saved to ${NAME}_debug.json" echo "$RESPONSE" > "/tmp/${NAME}_debug.json"
echo "Response saved to /tmp/${NAME}_debug.json"
echo "## Server: $NAME (⚠️ Error)" >> "$OUTPUT_FILE" echo "## Server: $NAME (⚠️ Error)" >> "$OUTPUT_FILE"
echo "Failed to retrieve data." >> "$OUTPUT_FILE" echo "Failed to retrieve data." >> "$OUTPUT_FILE"
return return
@@ -115,32 +132,30 @@ process_server() {
# Array capacity # Array capacity
ARRAY_TOTAL=$(echo "$RESPONSE" | jq -r '.data.array.capacity.kilobytes.total') ARRAY_TOTAL=$(echo "$RESPONSE" | jq -r '.data.array.capacity.kilobytes.total')
ARRAY_FREE=$(echo "$RESPONSE" | jq -r '.data.array.capacity.kilobytes.free')
ARRAY_USED=$(echo "$RESPONSE" | jq -r '.data.array.capacity.kilobytes.used') ARRAY_USED=$(echo "$RESPONSE" | jq -r '.data.array.capacity.kilobytes.used')
if [ "$ARRAY_TOTAL" != "null" ] && [ "$ARRAY_TOTAL" -gt 0 ]; then if [ "$ARRAY_TOTAL" != "null" ] && [ "$ARRAY_TOTAL" -gt 0 ]; then
ARRAY_TOTAL_GB=$((ARRAY_TOTAL / 1024 / 1024)) ARRAY_TOTAL_GB=$((ARRAY_TOTAL / 1024 / 1024))
ARRAY_FREE_GB=$((ARRAY_FREE / 1024 / 1024))
ARRAY_USED_GB=$((ARRAY_USED / 1024 / 1024)) ARRAY_USED_GB=$((ARRAY_USED / 1024 / 1024))
ARRAY_USED_PCT=$((ARRAY_USED * 100 / ARRAY_TOTAL)) ARRAY_USED_PCT=$((ARRAY_USED * 100 / ARRAY_TOTAL))
echo "### Storage" >> "$OUTPUT_FILE" echo "### Storage" >> "$OUTPUT_FILE"
echo "- **Array:** ${ARRAY_USED_GB}GB / ${ARRAY_TOTAL_GB}GB used (${ARRAY_USED_PCT}%)" >> "$OUTPUT_FILE" echo "- **Array:** ${ARRAY_USED_GB}GB / ${ARRAY_TOTAL_GB}GB used (${ARRAY_USED_PCT}%)" >> "$OUTPUT_FILE"
fi
# Cache pools # Cache pools
echo "- **Cache Pools:**" >> "$OUTPUT_FILE" echo "- **Cache Pools:**" >> "$OUTPUT_FILE"
echo "$RESPONSE" | jq -r '.data.array.caches[] | " - \(.name) (\(.device)): \(.temp)°C - \(.status) - \(if .fsSize then "\((.fsUsed / 1024 / 1024 | floor))GB / \((.fsSize / 1024 / 1024 | floor))GB used" else "N/A" end)"' >> "$OUTPUT_FILE" echo "$RESPONSE" | jq -r '(.data.array.caches // [])[] | " - \(.name) (\(.device)): \(.temp)°C - \(.status) - \(if .fsSize then "\((.fsUsed / 1024 / 1024 | floor))GB / \((.fsSize / 1024 / 1024 | floor))GB used" else "N/A" end)"' >> "$OUTPUT_FILE"
fi
# Docker # Docker
TOTAL_CONTAINERS=$(echo "$RESPONSE" | jq '[.data.docker.containers[]] | length') TOTAL_CONTAINERS=$(echo "$RESPONSE" | jq '[(.data.docker.containers // [])[]] | length')
RUNNING_CONTAINERS=$(echo "$RESPONSE" | jq '[.data.docker.containers[] | select(.state == "RUNNING")] | length') RUNNING_CONTAINERS=$(echo "$RESPONSE" | jq '[(.data.docker.containers // [])[] | select(.state == "RUNNING")] | length')
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
echo "### Workloads" >> "$OUTPUT_FILE" echo "### Workloads" >> "$OUTPUT_FILE"
echo "- **Docker:** ${TOTAL_CONTAINERS} containers (${RUNNING_CONTAINERS} running)" >> "$OUTPUT_FILE" echo "- **Docker:** ${TOTAL_CONTAINERS} containers (${RUNNING_CONTAINERS} running)" >> "$OUTPUT_FILE"
# Unhealthy containers # Unhealthy containers
UNHEALTHY=$(echo "$RESPONSE" | jq -r '.data.docker.containers[] | select(.status | test("unhealthy|restarting"; "i")) | " - ⚠️ \(.names[0]): \(.status)"') UNHEALTHY=$(echo "$RESPONSE" | jq -r '(.data.docker.containers // [])[] | select(.status | test("unhealthy|restarting"; "i")) | " - ⚠️ \(.names[0]): \(.status)"')
if [ -n "$UNHEALTHY" ]; then if [ -n "$UNHEALTHY" ]; then
echo "$UNHEALTHY" >> "$OUTPUT_FILE" echo "$UNHEALTHY" >> "$OUTPUT_FILE"
fi fi
@@ -173,7 +188,7 @@ process_server() {
echo "### Notifications" >> "$OUTPUT_FILE" echo "### Notifications" >> "$OUTPUT_FILE"
NOTIF_COUNT=$(echo "$RESPONSE" | jq '[.data.notifications[]] | length' 2>/dev/null || echo "0") NOTIF_COUNT=$(echo "$RESPONSE" | jq '[.data.notifications[]] | length' 2>/dev/null || echo "0")
if [ "$NOTIF_COUNT" -gt 0 ] && [ "$NOTIF_COUNT" != "null" ]; then if [ "$NOTIF_COUNT" != "null" ] && [ -n "$NOTIF_COUNT" ] && [ "$NOTIF_COUNT" -gt 0 ]; then
# Show recent notifications (last 10) # Show recent notifications (last 10)
ALERT_NOTIFS=$(echo "$RESPONSE" | jq -r '.data.notifications | sort_by(.timestamp) | reverse | .[0:10][] | "- [\(.importance // "info")] \(.title // .subject): \(.description // "No description") (\(.timestamp | split("T")[0]))"' 2>/dev/null) ALERT_NOTIFS=$(echo "$RESPONSE" | jq -r '.data.notifications | sort_by(.timestamp) | reverse | .[0:10][] | "- [\(.importance // "info")] \(.title // .subject): \(.description // "No description") (\(.timestamp | split("T")[0]))"' 2>/dev/null)
if [ -n "$ALERT_NOTIFS" ]; then if [ -n "$ALERT_NOTIFS" ]; then
@@ -198,7 +213,7 @@ process_server() {
} }
# Main loop - process each server from environment variables # Main loop - process each server from environment variables
for server in "TOOTIE" "SHART"; do for server in "${SERVERS[@]}"; do
name_var="UNRAID_${server}_NAME" name_var="UNRAID_${server}_NAME"
url_var="UNRAID_${server}_URL" url_var="UNRAID_${server}_URL"
key_var="UNRAID_${server}_API_KEY" key_var="UNRAID_${server}_API_KEY"

View File

@@ -16,6 +16,8 @@ OPTIONS:
-k, --key KEY API key (required) -k, --key KEY API key (required)
-q, --query QUERY GraphQL query (required) -q, --query QUERY GraphQL query (required)
-f, --format FORMAT Output format: json (default), raw, pretty -f, --format FORMAT Output format: json (default), raw, pretty
--ignore-errors Continue if GraphQL returns data alongside errors
--insecure Disable TLS certificate verification
-h, --help Show this help message -h, --help Show this help message
ENVIRONMENT VARIABLES: ENVIRONMENT VARIABLES:
@@ -43,6 +45,8 @@ URL="${UNRAID_URL:-}"
API_KEY="${UNRAID_API_KEY:-}" API_KEY="${UNRAID_API_KEY:-}"
QUERY="" QUERY=""
FORMAT="json" FORMAT="json"
IGNORE_ERRORS="${IGNORE_ERRORS:-false}"
INSECURE="false"
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -63,6 +67,14 @@ while [[ $# -gt 0 ]]; do
FORMAT="$2" FORMAT="$2"
shift 2 shift 2
;; ;;
--ignore-errors)
IGNORE_ERRORS="true"
shift
;;
--insecure)
INSECURE="true"
shift
;;
-h|--help) -h|--help)
usage usage
;; ;;
@@ -89,15 +101,22 @@ if [[ -z "$QUERY" ]]; then
exit 1 exit 1
fi fi
# Build JSON payload with proper escaping
PAYLOAD=$(jq -n --arg q "$QUERY" '{"query": $q}')
# Build curl flags
CURL_FLAGS=("-sL" "-X" "POST")
[[ "$INSECURE" == "true" ]] && CURL_FLAGS+=("-k")
# Make the request # Make the request
RESPONSE=$(curl -skL -X POST "$URL" \ RESPONSE=$(curl "${CURL_FLAGS[@]}" "$URL" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \ -H "x-api-key: $API_KEY" \
-d "{\"query\":\"$QUERY\"}") -d "$PAYLOAD")
# Check for errors # Check for errors
if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
# If we have data despite errors, and --ignore-errors is set, continue # If we have data despite errors and --ignore-errors was passed, continue
if [[ "$IGNORE_ERRORS" == "true" ]] && echo "$RESPONSE" | jq -e '.data' > /dev/null 2>&1; then if [[ "$IGNORE_ERRORS" == "true" ]] && echo "$RESPONSE" | jq -e '.data' > /dev/null 2>&1; then
echo "GraphQL Warning:" >&2 echo "GraphQL Warning:" >&2
echo "$RESPONSE" | jq -r '.errors[0].message' >&2 echo "$RESPONSE" | jq -r '.errors[0].message' >&2

View File

@@ -1,5 +1,6 @@
"""Shared test fixtures and helpers for Unraid MCP server tests.""" """Shared test fixtures and helpers for Unraid MCP server tests."""
from collections.abc import Generator
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@@ -8,7 +9,7 @@ from fastmcp import FastMCP
@pytest.fixture @pytest.fixture
def mock_graphql_request() -> AsyncMock: def mock_graphql_request() -> Generator[AsyncMock, None, None]:
"""Fixture that patches make_graphql_request at the core module. """Fixture that patches make_graphql_request at the core module.
NOTE: Since each tool file imports make_graphql_request into its own NOTE: Since each tool file imports make_graphql_request into its own
@@ -47,4 +48,4 @@ def make_tool_fn(
register_fn = getattr(module, register_fn_name) register_fn = getattr(module, register_fn_name)
test_mcp = FastMCP("test") test_mcp = FastMCP("test")
register_fn(test_mcp) register_fn(test_mcp)
return test_mcp._tool_manager._tools[tool_name].fn return test_mcp._tool_manager._tools[tool_name].fn # type: ignore[union-attr]

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_array tool.""" """Tests for unraid_array tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.array.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.array.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -49,10 +50,10 @@ class TestArrayActions:
call_args = _mock_graphql.call_args call_args = _mock_graphql.call_args
assert call_args[0][1] == {"correct": True} assert call_args[0][1] == {"correct": True}
async def test_parity_history(self, _mock_graphql: AsyncMock) -> None: async def test_parity_status(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"array": {"parityCheckStatus": {"progress": 50}}} _mock_graphql.return_value = {"array": {"parityCheckStatus": {"progress": 50}}}
tool_fn = _make_tool() tool_fn = _make_tool()
result = await tool_fn(action="parity_history") result = await tool_fn(action="parity_status")
assert result["success"] is True assert result["success"] is True
async def test_mount_disk(self, _mock_graphql: AsyncMock) -> None: async def test_mount_disk(self, _mock_graphql: AsyncMock) -> None:
@@ -100,4 +101,70 @@ class TestArrayActions:
_mock_graphql.side_effect = RuntimeError("disk error") _mock_graphql.side_effect = RuntimeError("disk error")
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="disk error"): with pytest.raises(ToolError, match="disk error"):
await tool_fn(action="parity_history") await tool_fn(action="parity_status")
class TestArrayMutationFailures:
"""Tests for mutation responses that indicate failure."""
async def test_start_mutation_returns_false(self, _mock_graphql: AsyncMock) -> None:
"""Mutation returning False in the response field should still succeed (the tool
wraps the raw response; it doesn't inspect the inner boolean)."""
_mock_graphql.return_value = {"setState": False}
tool_fn = _make_tool()
result = await tool_fn(action="start", confirm=True)
assert result["success"] is True
assert result["data"] == {"setState": False}
async def test_start_mutation_returns_null(self, _mock_graphql: AsyncMock) -> None:
"""Mutation returning null for the response field."""
_mock_graphql.return_value = {"setState": None}
tool_fn = _make_tool()
result = await tool_fn(action="start", confirm=True)
assert result["success"] is True
assert result["data"] == {"setState": None}
async def test_start_mutation_returns_empty_object(self, _mock_graphql: AsyncMock) -> None:
"""Mutation returning an empty object for the response field."""
_mock_graphql.return_value = {"setState": {}}
tool_fn = _make_tool()
result = await tool_fn(action="start", confirm=True)
assert result["success"] is True
assert result["data"] == {"setState": {}}
async def test_mount_disk_mutation_returns_false(self, _mock_graphql: AsyncMock) -> None:
"""mountArrayDisk returning False indicates mount failed."""
_mock_graphql.return_value = {"mountArrayDisk": False}
tool_fn = _make_tool()
result = await tool_fn(action="mount_disk", disk_id="disk:1")
assert result["success"] is True
assert result["data"]["mountArrayDisk"] is False
async def test_mutation_timeout(self, _mock_graphql: AsyncMock) -> None:
"""Mid-operation timeout should be wrapped in ToolError."""
_mock_graphql.side_effect = TimeoutError("operation timed out")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="timed out"):
await tool_fn(action="shutdown", confirm=True)
class TestArrayNetworkErrors:
"""Tests for network-level failures in array operations."""
async def test_http_500_server_error(self, _mock_graphql: AsyncMock) -> None:
"""HTTP 500 from the API should be wrapped in ToolError."""
mock_response = AsyncMock()
mock_response.status_code = 500
mock_response.text = "Internal Server Error"
_mock_graphql.side_effect = ToolError("HTTP error 500: Internal Server Error")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="HTTP error 500"):
await tool_fn(action="start", confirm=True)
async def test_connection_refused(self, _mock_graphql: AsyncMock) -> None:
"""Connection refused should be wrapped in ToolError."""
_mock_graphql.side_effect = ToolError("Network connection error: Connection refused")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Network connection error"):
await tool_fn(action="parity_status")

474
tests/test_client.py Normal file
View File

@@ -0,0 +1,474 @@
"""Tests for unraid_mcp.core.client — GraphQL client infrastructure."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from unraid_mcp.core.client import (
DEFAULT_TIMEOUT,
DISK_TIMEOUT,
_redact_sensitive,
is_idempotent_error,
make_graphql_request,
)
from unraid_mcp.core.exceptions import ToolError
# ---------------------------------------------------------------------------
# is_idempotent_error
# ---------------------------------------------------------------------------
class TestIsIdempotentError:
"""Verify all idempotent error pattern matches."""
def test_start_already_started(self) -> None:
assert is_idempotent_error("Container already started", "start") is True
def test_start_container_already_running(self) -> None:
assert is_idempotent_error("container already running", "start") is True
def test_start_http_code_304(self) -> None:
assert is_idempotent_error("HTTP code 304 - not modified", "start") is True
def test_stop_already_stopped(self) -> None:
assert is_idempotent_error("Container already stopped", "stop") is True
def test_stop_not_running(self) -> None:
assert is_idempotent_error("container not running", "stop") is True
def test_stop_http_code_304(self) -> None:
assert is_idempotent_error("HTTP code 304", "stop") is True
def test_start_unrelated_error(self) -> None:
assert is_idempotent_error("permission denied", "start") is False
def test_stop_unrelated_error(self) -> None:
assert is_idempotent_error("image not found", "stop") is False
def test_unknown_operation(self) -> None:
assert is_idempotent_error("already started", "restart") is False
def test_case_insensitive(self) -> None:
assert is_idempotent_error("ALREADY STARTED", "start") is True
assert is_idempotent_error("ALREADY STOPPED", "stop") is True
# ---------------------------------------------------------------------------
# _redact_sensitive
# ---------------------------------------------------------------------------
class TestRedactSensitive:
"""Verify recursive redaction of sensitive keys."""
def test_flat_dict(self) -> None:
data = {"username": "admin", "password": "hunter2", "host": "10.0.0.1"}
result = _redact_sensitive(data)
assert result["username"] == "admin"
assert result["password"] == "***"
assert result["host"] == "10.0.0.1"
def test_nested_dict(self) -> None:
data = {"config": {"apiKey": "abc123", "url": "http://host"}}
result = _redact_sensitive(data)
assert result["config"]["apiKey"] == "***"
assert result["config"]["url"] == "http://host"
def test_list_of_dicts(self) -> None:
data = [{"token": "t1"}, {"name": "safe"}]
result = _redact_sensitive(data)
assert result[0]["token"] == "***"
assert result[1]["name"] == "safe"
def test_deeply_nested(self) -> None:
data = {"a": {"b": {"c": {"secret": "deep"}}}}
result = _redact_sensitive(data)
assert result["a"]["b"]["c"]["secret"] == "***"
def test_non_dict_passthrough(self) -> None:
assert _redact_sensitive("plain_string") == "plain_string"
assert _redact_sensitive(42) == 42
assert _redact_sensitive(None) is None
def test_case_insensitive_keys(self) -> None:
data = {"Password": "p1", "TOKEN": "t1", "ApiKey": "k1", "Secret": "s1", "Key": "x1"}
result = _redact_sensitive(data)
for v in result.values():
assert v == "***"
def test_compound_key_names(self) -> None:
"""Keys containing sensitive substrings (e.g. 'user_password') are redacted."""
data = {
"user_password": "p1",
"api_key_value": "k1",
"auth_token_expiry": "t1",
"client_secret_id": "s1",
"username": "safe",
"host": "safe",
}
result = _redact_sensitive(data)
assert result["user_password"] == "***"
assert result["api_key_value"] == "***"
assert result["auth_token_expiry"] == "***"
assert result["client_secret_id"] == "***"
assert result["username"] == "safe"
assert result["host"] == "safe"
def test_mixed_list_content(self) -> None:
data = [{"key": "val"}, "string", 123, [{"token": "inner"}]]
result = _redact_sensitive(data)
assert result[0]["key"] == "***"
assert result[1] == "string"
assert result[2] == 123
assert result[3][0]["token"] == "***"
# ---------------------------------------------------------------------------
# Timeout constants
# ---------------------------------------------------------------------------
class TestTimeoutConstants:
def test_default_timeout_read(self) -> None:
assert DEFAULT_TIMEOUT.read == 30.0
def test_default_timeout_connect(self) -> None:
assert DEFAULT_TIMEOUT.connect == 5.0
def test_disk_timeout_read(self) -> None:
assert DISK_TIMEOUT.read == 90.0
def test_disk_timeout_connect(self) -> None:
assert DISK_TIMEOUT.connect == 5.0
# ---------------------------------------------------------------------------
# make_graphql_request — success paths
# ---------------------------------------------------------------------------
class TestMakeGraphQLRequestSuccess:
"""Test successful request paths."""
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
):
yield
async def test_simple_query(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {"data": {"info": {"os": "Unraid"}}}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request("{ info { os } }")
assert result == {"info": {"os": "Unraid"}}
async def test_query_with_variables(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {"data": {"container": {"name": "plex"}}}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request(
"query ($id: String!) { container(id: $id) { name } }",
variables={"id": "abc123"},
)
assert result == {"container": {"name": "plex"}}
# Verify variables were passed in the payload
call_kwargs = mock_client.post.call_args
assert call_kwargs.kwargs["json"]["variables"] == {"id": "abc123"}
async def test_custom_timeout_passed(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {"data": {}}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
custom_timeout = httpx.Timeout(10.0, read=90.0)
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
await make_graphql_request("{ info }", custom_timeout=custom_timeout)
call_kwargs = mock_client.post.call_args
assert call_kwargs.kwargs["timeout"] is custom_timeout
async def test_empty_data_returns_empty_dict(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {"data": None}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request("{ info }")
assert result == {}
async def test_missing_data_key_returns_empty_dict(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request("{ info }")
assert result == {}
# ---------------------------------------------------------------------------
# make_graphql_request — error paths
# ---------------------------------------------------------------------------
class TestMakeGraphQLRequestErrors:
"""Test error handling in make_graphql_request."""
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
):
yield
async def test_missing_api_url(self) -> None:
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", ""),
pytest.raises(ToolError, match="UNRAID_API_URL not configured"),
):
await make_graphql_request("{ info }")
async def test_missing_api_key(self) -> None:
with (
patch("unraid_mcp.core.client.UNRAID_API_KEY", ""),
pytest.raises(ToolError, match="UNRAID_API_KEY not configured"),
):
await make_graphql_request("{ info }")
async def test_http_401_error(self) -> None:
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.text = "Unauthorized"
http_error = httpx.HTTPStatusError(
"401 Unauthorized", request=MagicMock(), response=mock_response
)
mock_response.raise_for_status.side_effect = http_error
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="HTTP error 401"),
):
await make_graphql_request("{ info }")
async def test_http_500_error(self) -> None:
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.text = "Internal Server Error"
http_error = httpx.HTTPStatusError(
"500 Internal Server Error", request=MagicMock(), response=mock_response
)
mock_response.raise_for_status.side_effect = http_error
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="HTTP error 500"),
):
await make_graphql_request("{ info }")
async def test_http_503_error(self) -> None:
mock_response = MagicMock()
mock_response.status_code = 503
mock_response.text = "Service Unavailable"
http_error = httpx.HTTPStatusError(
"503 Service Unavailable", request=MagicMock(), response=mock_response
)
mock_response.raise_for_status.side_effect = http_error
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="HTTP error 503"),
):
await make_graphql_request("{ info }")
async def test_network_connection_refused(self) -> None:
mock_client = AsyncMock()
mock_client.post.side_effect = httpx.ConnectError("Connection refused")
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Network connection error"),
):
await make_graphql_request("{ info }")
async def test_network_timeout(self) -> None:
mock_client = AsyncMock()
mock_client.post.side_effect = httpx.ReadTimeout("Read timed out")
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Network connection error"),
):
await make_graphql_request("{ info }")
async def test_json_decode_error(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Invalid JSON response"),
):
await make_graphql_request("{ info }")
# ---------------------------------------------------------------------------
# make_graphql_request — GraphQL error handling
# ---------------------------------------------------------------------------
class TestGraphQLErrorHandling:
"""Test GraphQL-level error parsing and idempotent handling."""
@pytest.fixture(autouse=True)
def _patch_config(self):
with (
patch("unraid_mcp.core.client.UNRAID_API_URL", "https://unraid.local/graphql"),
patch("unraid_mcp.core.client.UNRAID_API_KEY", "test-key"),
):
yield
async def test_graphql_error_raises_tool_error(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [{"message": "Field 'bogus' not found"}]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Field 'bogus' not found"),
):
await make_graphql_request("{ bogus }")
async def test_multiple_graphql_errors_joined(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [
{"message": "Error one"},
{"message": "Error two"},
]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Error one; Error two"),
):
await make_graphql_request("{ info }")
async def test_idempotent_start_returns_success(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [{"message": "Container already running"}]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request(
'mutation { docker { start(id: "x") } }',
operation_context={"operation": "start"},
)
assert result["idempotent_success"] is True
assert result["operation"] == "start"
async def test_idempotent_stop_returns_success(self) -> None:
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [{"message": "Container not running"}]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with patch("unraid_mcp.core.client.get_http_client", return_value=mock_client):
result = await make_graphql_request(
'mutation { docker { stop(id: "x") } }',
operation_context={"operation": "stop"},
)
assert result["idempotent_success"] is True
assert result["operation"] == "stop"
async def test_non_idempotent_error_with_context_raises(self) -> None:
"""An error that doesn't match idempotent patterns still raises even with context."""
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [{"message": "Permission denied"}]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="Permission denied"),
):
await make_graphql_request(
'mutation { docker { start(id: "x") } }',
operation_context={"operation": "start"},
)
async def test_graphql_error_without_message_key(self) -> None:
"""Error objects without a 'message' key fall back to str()."""
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"errors": [{"code": "UNKNOWN", "detail": "something broke"}]
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
with (
patch("unraid_mcp.core.client.get_http_client", return_value=mock_client),
pytest.raises(ToolError, match="GraphQL API error"),
):
await make_graphql_request("{ info }")

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_docker tool.""" """Tests for unraid_docker tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -8,6 +9,7 @@ from conftest import make_tool_fn
from unraid_mcp.core.exceptions import ToolError from unraid_mcp.core.exceptions import ToolError
from unraid_mcp.tools.docker import find_container_by_identifier, get_available_container_names from unraid_mcp.tools.docker import find_container_by_identifier, get_available_container_names
# --- Unit tests for helpers --- # --- Unit tests for helpers ---
@@ -52,7 +54,7 @@ class TestGetAvailableContainerNames:
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.docker.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.docker.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -204,3 +206,92 @@ class TestDockerActions:
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="unexpected failure"): with pytest.raises(ToolError, match="unexpected failure"):
await tool_fn(action="list") await tool_fn(action="list")
class TestDockerMutationFailures:
"""Tests for mutation responses that indicate failure or unexpected shapes."""
async def test_remove_mutation_returns_null(self, _mock_graphql: AsyncMock) -> None:
"""removeContainer returning null instead of True."""
cid = "a" * 64 + ":local"
_mock_graphql.side_effect = [
{"docker": {"containers": [{"id": cid, "names": ["old-app"]}]}},
{"docker": {"removeContainer": None}},
]
tool_fn = _make_tool()
result = await tool_fn(action="remove", container_id="old-app", confirm=True)
assert result["success"] is True
assert result["container"] is None
async def test_start_mutation_empty_docker_response(self, _mock_graphql: AsyncMock) -> None:
"""docker field returning empty object (missing the action sub-field)."""
cid = "a" * 64 + ":local"
_mock_graphql.side_effect = [
{"docker": {"containers": [{"id": cid, "names": ["plex"]}]}},
{"docker": {}},
]
tool_fn = _make_tool()
result = await tool_fn(action="start", container_id="plex")
assert result["success"] is True
assert result["container"] is None
async def test_stop_mutation_returns_false_state(self, _mock_graphql: AsyncMock) -> None:
"""Stop mutation returning a container with unexpected state."""
cid = "a" * 64 + ":local"
_mock_graphql.side_effect = [
{"docker": {"containers": [{"id": cid, "names": ["plex"]}]}},
{"docker": {"stop": {"id": cid, "state": "running"}}},
]
tool_fn = _make_tool()
result = await tool_fn(action="stop", container_id="plex")
assert result["success"] is True
assert result["container"]["state"] == "running"
async def test_update_all_returns_empty_list(self, _mock_graphql: AsyncMock) -> None:
"""update_all with no containers to update."""
_mock_graphql.return_value = {"docker": {"updateAllContainers": []}}
tool_fn = _make_tool()
result = await tool_fn(action="update_all")
assert result["success"] is True
assert result["containers"] == []
async def test_mutation_timeout(self, _mock_graphql: AsyncMock) -> None:
"""Mid-operation timeout during a docker mutation."""
cid = "a" * 64 + ":local"
_mock_graphql.side_effect = [
{"docker": {"containers": [{"id": cid, "names": ["plex"]}]}},
TimeoutError("operation timed out"),
]
tool_fn = _make_tool()
with pytest.raises(ToolError, match="timed out"):
await tool_fn(action="start", container_id="plex")
class TestDockerNetworkErrors:
"""Tests for network-level failures in docker operations."""
async def test_list_connection_refused(self, _mock_graphql: AsyncMock) -> None:
"""Connection refused when listing containers should be wrapped in ToolError."""
_mock_graphql.side_effect = ToolError(
"Network connection error: [Errno 111] Connection refused"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Connection refused"):
await tool_fn(action="list")
async def test_start_http_401_unauthorized(self, _mock_graphql: AsyncMock) -> None:
"""HTTP 401 should propagate as ToolError."""
_mock_graphql.side_effect = ToolError("HTTP error 401: Unauthorized")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="401"):
await tool_fn(action="list")
async def test_json_decode_error_on_list(self, _mock_graphql: AsyncMock) -> None:
"""Invalid JSON response should be wrapped in ToolError."""
_mock_graphql.side_effect = ToolError(
"Invalid JSON response from Unraid API: Expecting value: line 1 column 1"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Invalid JSON"):
await tool_fn(action="list")

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_health tool.""" """Tests for unraid_health tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.health.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.health.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -104,8 +105,7 @@ class TestHealthActions:
with patch( with patch(
"unraid_mcp.tools.health._diagnose_subscriptions", "unraid_mcp.tools.health._diagnose_subscriptions",
side_effect=RuntimeError("broken"), side_effect=RuntimeError("broken"),
): ), pytest.raises(ToolError, match="broken"):
with pytest.raises(ToolError, match="broken"):
await tool_fn(action="diagnose") await tool_fn(action="diagnose")
async def test_diagnose_success(self, _mock_graphql: AsyncMock) -> None: async def test_diagnose_success(self, _mock_graphql: AsyncMock) -> None:
@@ -120,17 +120,24 @@ class TestHealthActions:
async def test_diagnose_import_error_internal(self) -> None: async def test_diagnose_import_error_internal(self) -> None:
"""_diagnose_subscriptions catches ImportError and returns error dict.""" """_diagnose_subscriptions catches ImportError and returns error dict."""
import builtins import sys
from unraid_mcp.tools.health import _diagnose_subscriptions from unraid_mcp.tools.health import _diagnose_subscriptions
real_import = builtins.__import__ # Remove cached subscription modules so the import is re-triggered
cached = {k: v for k, v in sys.modules.items() if "unraid_mcp.subscriptions" in k}
for k in cached:
del sys.modules[k]
def fail_subscriptions(name, *args, **kwargs): try:
if "subscriptions" in name: # Replace the modules with objects that raise ImportError on access
raise ImportError("no module") with patch.dict(sys.modules, {
return real_import(name, *args, **kwargs) "unraid_mcp.subscriptions": None,
"unraid_mcp.subscriptions.manager": None,
with patch("builtins.__import__", side_effect=fail_subscriptions): "unraid_mcp.subscriptions.resources": None,
}):
result = await _diagnose_subscriptions() result = await _diagnose_subscriptions()
assert "error" in result assert "error" in result
finally:
# Restore cached modules
sys.modules.update(cached)

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_info tool.""" """Tests for unraid_info tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -12,6 +13,7 @@ from unraid_mcp.tools.info import (
_process_system_info, _process_system_info,
) )
# --- Unit tests for helper functions --- # --- Unit tests for helper functions ---
@@ -115,7 +117,7 @@ class TestProcessArrayStatus:
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.info.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.info.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -207,3 +209,32 @@ class TestUnraidInfoTool:
result = await tool_fn(action="ups_devices") result = await tool_fn(action="ups_devices")
assert len(result["ups_devices"]) == 1 assert len(result["ups_devices"]) == 1
assert result["ups_devices"][0]["model"] == "APC" assert result["ups_devices"][0]["model"] == "APC"
class TestInfoNetworkErrors:
"""Tests for network-level failures in info operations."""
async def test_overview_http_401_unauthorized(self, _mock_graphql: AsyncMock) -> None:
"""HTTP 401 Unauthorized should propagate as ToolError."""
_mock_graphql.side_effect = ToolError("HTTP error 401: Unauthorized")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="401"):
await tool_fn(action="overview")
async def test_overview_connection_refused(self, _mock_graphql: AsyncMock) -> None:
"""Connection refused should propagate as ToolError."""
_mock_graphql.side_effect = ToolError(
"Network connection error: [Errno 111] Connection refused"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Connection refused"):
await tool_fn(action="overview")
async def test_network_json_decode_error(self, _mock_graphql: AsyncMock) -> None:
"""Invalid JSON from API should propagate as ToolError."""
_mock_graphql.side_effect = ToolError(
"Invalid JSON response from Unraid API: Expecting value"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Invalid JSON"):
await tool_fn(action="network")

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_keys tool.""" """Tests for unraid_keys tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.keys.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.keys.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -88,3 +89,9 @@ class TestKeysActions:
tool_fn = _make_tool() tool_fn = _make_tool()
result = await tool_fn(action="delete", key_id="k:1", confirm=True) result = await tool_fn(action="delete", key_id="k:1", confirm=True)
assert result["success"] is True assert result["success"] is True
async def test_generic_exception_wraps(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.side_effect = RuntimeError("connection lost")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="connection lost"):
await tool_fn(action="list")

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_notifications tool.""" """Tests for unraid_notifications tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.notifications.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.notifications.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_rclone tool.""" """Tests for unraid_rclone tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.rclone.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.rclone.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -18,18 +19,19 @@ def _make_tool():
return make_tool_fn("unraid_mcp.tools.rclone", "register_rclone_tool", "unraid_rclone") return make_tool_fn("unraid_mcp.tools.rclone", "register_rclone_tool", "unraid_rclone")
@pytest.mark.usefixtures("_mock_graphql")
class TestRcloneValidation: class TestRcloneValidation:
async def test_delete_requires_confirm(self, _mock_graphql: AsyncMock) -> None: async def test_delete_requires_confirm(self) -> None:
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="destructive"): with pytest.raises(ToolError, match="destructive"):
await tool_fn(action="delete_remote", name="gdrive") await tool_fn(action="delete_remote", name="gdrive")
async def test_create_requires_fields(self, _mock_graphql: AsyncMock) -> None: async def test_create_requires_fields(self) -> None:
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="requires name"): with pytest.raises(ToolError, match="requires name"):
await tool_fn(action="create_remote") await tool_fn(action="create_remote")
async def test_delete_requires_name(self, _mock_graphql: AsyncMock) -> None: async def test_delete_requires_name(self) -> None:
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="name is required"): with pytest.raises(ToolError, match="name is required"):
await tool_fn(action="delete_remote", confirm=True) await tool_fn(action="delete_remote", confirm=True)

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_storage tool.""" """Tests for unraid_storage tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -36,7 +37,7 @@ class TestFormatBytes:
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.storage.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.storage.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -98,7 +99,7 @@ class TestStorageActions:
} }
tool_fn = _make_tool() tool_fn = _make_tool()
result = await tool_fn(action="disk_details", disk_id="d:1") result = await tool_fn(action="disk_details", disk_id="d:1")
assert result["summary"]["temperature"] == "35C" assert result["summary"]["temperature"] == "35\u00b0C"
assert "1.00 GB" in result["summary"]["size_formatted"] assert "1.00 GB" in result["summary"]["size_formatted"]
async def test_disk_details_not_found(self, _mock_graphql: AsyncMock) -> None: async def test_disk_details_not_found(self, _mock_graphql: AsyncMock) -> None:
@@ -124,3 +125,32 @@ class TestStorageActions:
tool_fn = _make_tool() tool_fn = _make_tool()
result = await tool_fn(action="logs", log_path="/var/log/syslog") result = await tool_fn(action="logs", log_path="/var/log/syslog")
assert result["content"] == "log line" assert result["content"] == "log line"
class TestStorageNetworkErrors:
"""Tests for network-level failures in storage operations."""
async def test_logs_json_decode_error(self, _mock_graphql: AsyncMock) -> None:
"""Invalid JSON response when reading logs should propagate as ToolError."""
_mock_graphql.side_effect = ToolError(
"Invalid JSON response from Unraid API: Expecting value: line 1 column 1"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Invalid JSON"):
await tool_fn(action="logs", log_path="/var/log/syslog")
async def test_shares_connection_refused(self, _mock_graphql: AsyncMock) -> None:
"""Connection refused when listing shares should propagate as ToolError."""
_mock_graphql.side_effect = ToolError(
"Network connection error: [Errno 111] Connection refused"
)
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Connection refused"):
await tool_fn(action="shares")
async def test_disks_http_500(self, _mock_graphql: AsyncMock) -> None:
"""HTTP 500 when listing disks should propagate as ToolError."""
_mock_graphql.side_effect = ToolError("HTTP error 500: Internal Server Error")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="HTTP error 500"):
await tool_fn(action="disks")

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_users tool.""" """Tests for unraid_users tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.users.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.users.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock

View File

@@ -1,5 +1,6 @@
"""Tests for unraid_vm tool.""" """Tests for unraid_vm tool."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
@pytest.fixture @pytest.fixture
def _mock_graphql() -> AsyncMock: def _mock_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.virtualization.make_graphql_request", new_callable=AsyncMock) as mock: with patch("unraid_mcp.tools.virtualization.make_graphql_request", new_callable=AsyncMock) as mock:
yield mock yield mock
@@ -128,3 +129,53 @@ class TestVmActions:
tool_fn = _make_tool() tool_fn = _make_tool()
with pytest.raises(ToolError, match="Failed to start"): with pytest.raises(ToolError, match="Failed to start"):
await tool_fn(action="start", vm_id="uuid-1") await tool_fn(action="start", vm_id="uuid-1")
class TestVmMutationFailures:
"""Tests for VM mutation responses indicating failure or unexpected shapes."""
async def test_start_mutation_empty_response(self, _mock_graphql: AsyncMock) -> None:
"""Empty top-level response (no 'vm' key) should raise ToolError."""
_mock_graphql.return_value = {}
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Failed to start"):
await tool_fn(action="start", vm_id="uuid-1")
async def test_start_mutation_returns_false(self, _mock_graphql: AsyncMock) -> None:
"""VM start returning False should still succeed (the tool reports the raw value)."""
_mock_graphql.return_value = {"vm": {"start": False}}
tool_fn = _make_tool()
result = await tool_fn(action="start", vm_id="uuid-1")
assert result["success"] is False
assert result["action"] == "start"
async def test_stop_mutation_returns_null(self, _mock_graphql: AsyncMock) -> None:
"""VM stop returning None in the field should raise ToolError (field not 'in' data)."""
_mock_graphql.return_value = {"vm": {"stop": None}}
tool_fn = _make_tool()
# The check is `field in data["vm"]` — None is truthy for `in`, so it succeeds
result = await tool_fn(action="stop", vm_id="uuid-1")
assert result["success"] is None
assert result["action"] == "stop"
async def test_force_stop_mutation_empty_vm_object(self, _mock_graphql: AsyncMock) -> None:
"""Empty vm object with no matching field should raise ToolError."""
_mock_graphql.return_value = {"vm": {}}
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Failed to force_stop"):
await tool_fn(action="force_stop", vm_id="uuid-1", confirm=True)
async def test_reboot_mutation_vm_key_none(self, _mock_graphql: AsyncMock) -> None:
"""vm key being None should raise ToolError."""
_mock_graphql.return_value = {"vm": None}
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Failed to reboot"):
await tool_fn(action="reboot", vm_id="uuid-1")
async def test_mutation_timeout(self, _mock_graphql: AsyncMock) -> None:
"""Mid-operation timeout should be wrapped in ToolError."""
_mock_graphql.side_effect = TimeoutError("VM operation timed out")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="timed out"):
await tool_fn(action="start", vm_id="uuid-1")

View File

@@ -4,4 +4,4 @@ A modular MCP (Model Context Protocol) server that provides tools to interact
with an Unraid server's GraphQL API. with an Unraid server's GraphQL API.
""" """
__version__ = "0.1.0" __version__ = "0.2.0"

View File

@@ -5,8 +5,8 @@ that cap at 10MB and start over (no rotation) for consistent use across all modu
""" """
import logging import logging
import os
from datetime import datetime from datetime import datetime
from pathlib import Path
import pytz import pytz
from rich.align import Align from rich.align import Align
@@ -16,6 +16,7 @@ from rich.panel import Panel
from rich.rule import Rule from rich.rule import Rule
from rich.text import Text from rich.text import Text
try: try:
from fastmcp.utilities.logging import get_logger as get_fastmcp_logger from fastmcp.utilities.logging import get_logger as get_fastmcp_logger
FASTMCP_AVAILABLE = True FASTMCP_AVAILABLE = True
@@ -24,6 +25,7 @@ except ImportError:
from .settings import LOG_FILE_PATH, LOG_LEVEL_STR from .settings import LOG_FILE_PATH, LOG_LEVEL_STR
# Global Rich console for consistent formatting # Global Rich console for consistent formatting
console = Console(stderr=True, force_terminal=True) console = Console(stderr=True, force_terminal=True)
@@ -31,7 +33,7 @@ console = Console(stderr=True, force_terminal=True)
class OverwriteFileHandler(logging.FileHandler): class OverwriteFileHandler(logging.FileHandler):
"""Custom file handler that overwrites the log file when it reaches max size.""" """Custom file handler that overwrites the log file when it reaches max size."""
def __init__(self, filename, max_bytes=10*1024*1024, mode='a', encoding=None, delay=False): def __init__(self, filename, max_bytes=10*1024*1024, mode="a", encoding=None, delay=False):
"""Initialize the handler. """Initialize the handler.
Args: Args:
@@ -47,18 +49,19 @@ class OverwriteFileHandler(logging.FileHandler):
def emit(self, record): def emit(self, record):
"""Emit a record, checking file size and overwriting if needed.""" """Emit a record, checking file size and overwriting if needed."""
# Check file size before writing # Check file size before writing
if self.stream and hasattr(self.stream, 'name'): if self.stream and hasattr(self.stream, "name"):
try: try:
if os.path.exists(self.baseFilename): base_path = Path(self.baseFilename)
file_size = os.path.getsize(self.baseFilename) if base_path.exists():
file_size = base_path.stat().st_size
if file_size >= self.max_bytes: if file_size >= self.max_bytes:
# Close current stream # Close current stream
if self.stream: if self.stream:
self.stream.close() self.stream.close()
# Remove the old file and start fresh # Remove the old file and start fresh
if os.path.exists(self.baseFilename): if base_path.exists():
os.remove(self.baseFilename) base_path.unlink()
# Reopen with truncate mode # Reopen with truncate mode
self.stream = self._open() self.stream = self._open()
@@ -75,9 +78,10 @@ class OverwriteFileHandler(logging.FileHandler):
) )
super().emit(reset_record) super().emit(reset_record)
except OSError: except OSError as e:
# If there's an issue checking file size, just continue normally import sys
pass print(f"WARNING: Log file size check failed: {e}. Continuing without rotation.",
file=sys.stderr)
# Emit the original record # Emit the original record
super().emit(record) super().emit(record)
@@ -119,11 +123,11 @@ def setup_logger(name: str = "UnraidMCPServer") -> logging.Logger:
file_handler = OverwriteFileHandler( file_handler = OverwriteFileHandler(
LOG_FILE_PATH, LOG_FILE_PATH,
max_bytes=10*1024*1024, max_bytes=10*1024*1024,
encoding='utf-8' encoding="utf-8"
) )
file_handler.setLevel(numeric_log_level) file_handler.setLevel(numeric_log_level)
file_formatter = logging.Formatter( file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s"
) )
file_handler.setFormatter(file_formatter) file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
@@ -163,11 +167,11 @@ def configure_fastmcp_logger_with_rich() -> logging.Logger | None:
file_handler = OverwriteFileHandler( file_handler = OverwriteFileHandler(
LOG_FILE_PATH, LOG_FILE_PATH,
max_bytes=10*1024*1024, max_bytes=10*1024*1024,
encoding='utf-8' encoding="utf-8"
) )
file_handler.setLevel(numeric_log_level) file_handler.setLevel(numeric_log_level)
file_formatter = logging.Formatter( file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s"
) )
file_handler.setFormatter(file_formatter) file_handler.setFormatter(file_formatter)
fastmcp_logger.addHandler(file_handler) fastmcp_logger.addHandler(file_handler)
@@ -196,7 +200,7 @@ def configure_fastmcp_logger_with_rich() -> logging.Logger | None:
root_file_handler = OverwriteFileHandler( root_file_handler = OverwriteFileHandler(
LOG_FILE_PATH, LOG_FILE_PATH,
max_bytes=10*1024*1024, max_bytes=10*1024*1024,
encoding='utf-8' encoding="utf-8"
) )
root_file_handler.setLevel(numeric_log_level) root_file_handler.setLevel(numeric_log_level)
root_file_handler.setFormatter(file_formatter) root_file_handler.setFormatter(file_formatter)
@@ -225,12 +229,12 @@ def log_configuration_status(logger: logging.Logger) -> None:
config = get_config_summary() config = get_config_summary()
# Log configuration status # Log configuration status
if config['api_url_configured']: if config["api_url_configured"]:
logger.info(f"UNRAID_API_URL loaded: {config['api_url_preview']}") logger.info(f"UNRAID_API_URL loaded: {config['api_url_preview']}")
else: else:
logger.warning("UNRAID_API_URL not found in environment or .env file.") logger.warning("UNRAID_API_URL not found in environment or .env file.")
if config['api_key_configured']: if config["api_key_configured"]:
logger.info("UNRAID_API_KEY loaded: ****") # Don't log the key itself logger.info("UNRAID_API_KEY loaded: ****") # Don't log the key itself
else: else:
logger.warning("UNRAID_API_KEY not found in environment or .env file.") logger.warning("UNRAID_API_KEY not found in environment or .env file.")
@@ -240,14 +244,14 @@ def log_configuration_status(logger: logging.Logger) -> None:
logger.info(f"UNRAID_MCP_TRANSPORT set to: {config['transport']}") logger.info(f"UNRAID_MCP_TRANSPORT set to: {config['transport']}")
logger.info(f"UNRAID_MCP_LOG_LEVEL set to: {config['log_level']}") logger.info(f"UNRAID_MCP_LOG_LEVEL set to: {config['log_level']}")
if not config['config_valid']: if not config["config_valid"]:
logger.error(f"Missing required configuration: {config['missing_config']}") logger.error(f"Missing required configuration: {config['missing_config']}")
# Development logging helpers for Rich formatting # Development logging helpers for Rich formatting
def get_est_timestamp() -> str: def get_est_timestamp() -> str:
"""Get current timestamp in EST timezone with YY/MM/DD format.""" """Get current timestamp in EST timezone with YY/MM/DD format."""
est = pytz.timezone('US/Eastern') est = pytz.timezone("US/Eastern")
now = datetime.now(est) now = datetime.now(est)
return now.strftime("%y/%m/%d %H:%M:%S") return now.strftime("%y/%m/%d %H:%M:%S")
@@ -271,7 +275,7 @@ def log_with_level_and_indent(message: str, level: str = "info", indent: int = 0
"error": {"color": "#BF616A", "icon": "", "style": "bold"}, # Nordic red "error": {"color": "#BF616A", "icon": "", "style": "bold"}, # Nordic red
"warning": {"color": "#EBCB8B", "icon": "⚠️", "style": ""}, # Nordic yellow "warning": {"color": "#EBCB8B", "icon": "⚠️", "style": ""}, # Nordic yellow
"success": {"color": "#A3BE8C", "icon": "", "style": "bold"}, # Nordic green "success": {"color": "#A3BE8C", "icon": "", "style": "bold"}, # Nordic green
"info": {"color": "#5E81AC", "icon": "", "style": "bold"}, # Nordic blue (bold) "info": {"color": "#5E81AC", "icon": "\u2139\ufe0f", "style": "bold"}, # Nordic blue (bold)
"status": {"color": "#81A1C1", "icon": "🔍", "style": ""}, # Light Nordic blue "status": {"color": "#81A1C1", "icon": "🔍", "style": ""}, # Light Nordic blue
"debug": {"color": "#4C566A", "icon": "🐛", "style": ""}, # Nordic dark gray "debug": {"color": "#4C566A", "icon": "🐛", "style": ""}, # Nordic dark gray
} }
@@ -328,11 +332,7 @@ def log_status(message: str, indent: int = 0) -> None:
if FASTMCP_AVAILABLE: if FASTMCP_AVAILABLE:
# Use FastMCP logger with Rich formatting # Use FastMCP logger with Rich formatting
_fastmcp_logger = configure_fastmcp_logger_with_rich() _fastmcp_logger = configure_fastmcp_logger_with_rich()
if _fastmcp_logger is not None: logger = _fastmcp_logger if _fastmcp_logger is not None else setup_logger()
logger = _fastmcp_logger
else:
# Fallback to our custom logger if FastMCP configuration fails
logger = setup_logger()
else: else:
# Fallback to our custom logger if FastMCP is not available # Fallback to our custom logger if FastMCP is not available
logger = setup_logger() logger = setup_logger()

View File

@@ -10,6 +10,7 @@ from typing import Any
from dotenv import load_dotenv from dotenv import load_dotenv
# Get the script directory (config module location) # Get the script directory (config module location)
SCRIPT_DIR = Path(__file__).parent # /home/user/code/unraid-mcp/unraid_mcp/config/ SCRIPT_DIR = Path(__file__).parent # /home/user/code/unraid-mcp/unraid_mcp/config/
UNRAID_MCP_DIR = SCRIPT_DIR.parent # /home/user/code/unraid-mcp/unraid_mcp/ UNRAID_MCP_DIR = SCRIPT_DIR.parent # /home/user/code/unraid-mcp/unraid_mcp/
@@ -18,10 +19,10 @@ PROJECT_ROOT = UNRAID_MCP_DIR.parent # /home/user/code/unraid-mcp/
# Load environment variables from .env file # Load environment variables from .env file
# In container: First try /app/.env.local (mounted), then project root .env # In container: First try /app/.env.local (mounted), then project root .env
dotenv_paths = [ dotenv_paths = [
Path('/app/.env.local'), # Container mount point Path("/app/.env.local"), # Container mount point
PROJECT_ROOT / '.env.local', # Project root .env.local PROJECT_ROOT / ".env.local", # Project root .env.local
PROJECT_ROOT / '.env', # Project root .env PROJECT_ROOT / ".env", # Project root .env
UNRAID_MCP_DIR / '.env' # Local .env in unraid_mcp/ UNRAID_MCP_DIR / ".env" # Local .env in unraid_mcp/
] ]
for dotenv_path in dotenv_paths: for dotenv_path in dotenv_paths:
@@ -51,7 +52,7 @@ else: # Path to CA bundle
UNRAID_VERIFY_SSL = raw_verify_ssl UNRAID_VERIFY_SSL = raw_verify_ssl
# Logging Configuration # Logging Configuration
LOG_LEVEL_STR = os.getenv('UNRAID_MCP_LOG_LEVEL', 'INFO').upper() LOG_LEVEL_STR = os.getenv("UNRAID_MCP_LOG_LEVEL", "INFO").upper()
LOG_FILE_NAME = os.getenv("UNRAID_MCP_LOG_FILE", "unraid-mcp.log") LOG_FILE_NAME = os.getenv("UNRAID_MCP_LOG_FILE", "unraid-mcp.log")
LOGS_DIR = Path("/tmp") LOGS_DIR = Path("/tmp")
LOG_FILE_PATH = LOGS_DIR / LOG_FILE_NAME LOG_FILE_PATH = LOGS_DIR / LOG_FILE_NAME
@@ -61,8 +62,8 @@ LOGS_DIR.mkdir(parents=True, exist_ok=True)
# HTTP Client Configuration # HTTP Client Configuration
TIMEOUT_CONFIG = { TIMEOUT_CONFIG = {
'default': 30, "default": 30,
'disk_operations': 90, # Longer timeout for SMART data queries "disk_operations": 90, # Longer timeout for SMART data queries
} }
@@ -94,15 +95,15 @@ def get_config_summary() -> dict[str, Any]:
is_valid, missing = validate_required_config() is_valid, missing = validate_required_config()
return { return {
'api_url_configured': bool(UNRAID_API_URL), "api_url_configured": bool(UNRAID_API_URL),
'api_url_preview': UNRAID_API_URL[:20] + '...' if UNRAID_API_URL else None, "api_url_preview": UNRAID_API_URL[:20] + "..." if UNRAID_API_URL else None,
'api_key_configured': bool(UNRAID_API_KEY), "api_key_configured": bool(UNRAID_API_KEY),
'server_host': UNRAID_MCP_HOST, "server_host": UNRAID_MCP_HOST,
'server_port': UNRAID_MCP_PORT, "server_port": UNRAID_MCP_PORT,
'transport': UNRAID_MCP_TRANSPORT, "transport": UNRAID_MCP_TRANSPORT,
'ssl_verify': UNRAID_VERIFY_SSL, "ssl_verify": UNRAID_VERIFY_SSL,
'log_level': LOG_LEVEL_STR, "log_level": LOG_LEVEL_STR,
'log_file': str(LOG_FILE_PATH), "log_file": str(LOG_FILE_PATH),
'config_valid': is_valid, "config_valid": is_valid,
'missing_config': missing if not is_valid else None "missing_config": missing if not is_valid else None
} }

View File

@@ -20,14 +20,21 @@ from ..config.settings import (
) )
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
# Sensitive keys to redact from debug logs # Sensitive keys to redact from debug logs
_SENSITIVE_KEYS = {"password", "key", "secret", "token", "apikey"} _SENSITIVE_KEYS = {"password", "key", "secret", "token", "apikey"}
def _is_sensitive_key(key: str) -> bool:
"""Check if a key name contains any sensitive substring."""
key_lower = key.lower()
return any(s in key_lower for s in _SENSITIVE_KEYS)
def _redact_sensitive(obj: Any) -> Any: def _redact_sensitive(obj: Any) -> Any:
"""Recursively redact sensitive values from nested dicts/lists.""" """Recursively redact sensitive values from nested dicts/lists."""
if isinstance(obj, dict): if isinstance(obj, dict):
return {k: ("***" if k.lower() in _SENSITIVE_KEYS else _redact_sensitive(v)) for k, v in obj.items()} return {k: ("***" if _is_sensitive_key(k) else _redact_sensitive(v)) for k, v in obj.items()}
if isinstance(obj, list): if isinstance(obj, list):
return [_redact_sensitive(item) for item in obj] return [_redact_sensitive(item) for item in obj]
return obj return obj
@@ -35,7 +42,25 @@ def _redact_sensitive(obj: Any) -> Any:
# HTTP timeout configuration # HTTP timeout configuration
DEFAULT_TIMEOUT = httpx.Timeout(10.0, read=30.0, connect=5.0) DEFAULT_TIMEOUT = httpx.Timeout(10.0, read=30.0, connect=5.0)
DISK_TIMEOUT = httpx.Timeout(10.0, read=TIMEOUT_CONFIG['disk_operations'], connect=5.0) DISK_TIMEOUT = httpx.Timeout(10.0, read=TIMEOUT_CONFIG["disk_operations"], connect=5.0)
# Named timeout profiles
_TIMEOUT_PROFILES: dict[str, httpx.Timeout] = {
"default": DEFAULT_TIMEOUT,
"disk_operations": DISK_TIMEOUT,
}
def get_timeout_for_operation(profile: str) -> httpx.Timeout:
"""Get a timeout configuration by profile name.
Args:
profile: Timeout profile name (e.g., "default", "disk_operations")
Returns:
The matching httpx.Timeout, falling back to DEFAULT_TIMEOUT for unknown profiles
"""
return _TIMEOUT_PROFILES.get(profile, DEFAULT_TIMEOUT)
# Global connection pool (module-level singleton) # Global connection pool (module-level singleton)
_http_client: httpx.AsyncClient | None = None _http_client: httpx.AsyncClient | None = None
@@ -55,34 +80,30 @@ def is_idempotent_error(error_message: str, operation: str) -> bool:
error_lower = error_message.lower() error_lower = error_message.lower()
# Docker container operation patterns # Docker container operation patterns
if operation == 'start': if operation == "start":
return ( return (
'already started' in error_lower or "already started" in error_lower or
'container already running' in error_lower or "container already running" in error_lower or
'http code 304' in error_lower "http code 304" in error_lower
) )
elif operation == 'stop': if operation == "stop":
return ( return (
'already stopped' in error_lower or "already stopped" in error_lower or
'container already stopped' in error_lower or "container already stopped" in error_lower or
'container not running' in error_lower or "container not running" in error_lower or
'http code 304' in error_lower "http code 304" in error_lower
) )
return False return False
async def get_http_client() -> httpx.AsyncClient: async def _create_http_client() -> httpx.AsyncClient:
"""Get or create shared HTTP client with connection pooling. """Create a new HTTP client instance with connection pooling.
Returns: Returns:
Singleton AsyncClient instance with connection pooling enabled A new AsyncClient configured for Unraid API communication
""" """
global _http_client return httpx.AsyncClient(
async with _client_lock:
if _http_client is None or _http_client.is_closed:
_http_client = httpx.AsyncClient(
# Connection pool settings # Connection pool settings
limits=httpx.Limits( limits=httpx.Limits(
max_keepalive_connections=20, max_keepalive_connections=20,
@@ -99,9 +120,36 @@ async def get_http_client() -> httpx.AsyncClient:
"User-Agent": f"UnraidMCPServer/{VERSION}" "User-Agent": f"UnraidMCPServer/{VERSION}"
} }
) )
async def get_http_client() -> httpx.AsyncClient:
"""Get or create shared HTTP client with connection pooling.
The client is protected by an asyncio lock to prevent concurrent creation.
If the existing client was closed (e.g., during shutdown), a new one is created.
Returns:
Singleton AsyncClient instance with connection pooling enabled
"""
global _http_client
async with _client_lock:
if _http_client is None or _http_client.is_closed:
_http_client = await _create_http_client()
logger.info("Created shared HTTP client with connection pooling (20 keepalive, 100 max connections)") logger.info("Created shared HTTP client with connection pooling (20 keepalive, 100 max connections)")
return _http_client client = _http_client
# Verify client is still open after releasing the lock.
# In asyncio's cooperative model this is unlikely to fail, but guards
# against edge cases where close_http_client runs between yield points.
if client.is_closed:
async with _client_lock:
_http_client = await _create_http_client()
client = _http_client
logger.info("Re-created HTTP client after unexpected close")
return client
async def close_http_client() -> None: async def close_http_client() -> None:
@@ -175,12 +223,12 @@ async def make_graphql_request(
response.raise_for_status() # Raise an exception for HTTP error codes 4xx/5xx response.raise_for_status() # Raise an exception for HTTP error codes 4xx/5xx
response_data = response.json() response_data = response.json()
if "errors" in response_data and response_data["errors"]: if response_data.get("errors"):
error_details = "; ".join([err.get("message", str(err)) for err in response_data["errors"]]) error_details = "; ".join([err.get("message", str(err)) for err in response_data["errors"]])
# Check if this is an idempotent error that should be treated as success # Check if this is an idempotent error that should be treated as success
if operation_context and operation_context.get('operation'): if operation_context and operation_context.get("operation"):
operation = operation_context['operation'] operation = operation_context["operation"]
if is_idempotent_error(error_details, operation): if is_idempotent_error(error_details, operation):
logger.warning(f"Idempotent operation '{operation}' - treating as success: {error_details}") logger.warning(f"Idempotent operation '{operation}' - treating as success: {error_details}")
# Return a success response with the current state information # Return a success response with the current state information
@@ -204,22 +252,7 @@ async def make_graphql_request(
raise ToolError(f"HTTP error {e.response.status_code}: {e.response.text}") from e raise ToolError(f"HTTP error {e.response.status_code}: {e.response.text}") from e
except httpx.RequestError as e: except httpx.RequestError as e:
logger.error(f"Request error occurred: {e}") logger.error(f"Request error occurred: {e}")
raise ToolError(f"Network connection error: {str(e)}") from e raise ToolError(f"Network connection error: {e!s}") from e
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON response: {e}") logger.error(f"Failed to decode JSON response: {e}")
raise ToolError(f"Invalid JSON response from Unraid API: {str(e)}") from e raise ToolError(f"Invalid JSON response from Unraid API: {e!s}") from e
def get_timeout_for_operation(operation_type: str = "default") -> httpx.Timeout:
"""Get appropriate timeout configuration for different operation types.
Args:
operation_type: Type of operation ('default', 'disk_operations')
Returns:
httpx.Timeout configuration appropriate for the operation
"""
if operation_type == "disk_operations":
return DISK_TIMEOUT
else:
return DEFAULT_TIMEOUT

View File

@@ -6,6 +6,7 @@ the modular server implementation from unraid_mcp.server.
""" """
import asyncio import asyncio
import sys
async def shutdown_cleanup() -> None: async def shutdown_cleanup() -> None:
@@ -26,16 +27,20 @@ def main() -> None:
print("\nServer stopped by user") print("\nServer stopped by user")
try: try:
asyncio.run(shutdown_cleanup()) asyncio.run(shutdown_cleanup())
except RuntimeError: except RuntimeError as e:
# Event loop already closed, skip cleanup if "event loop is closed" in str(e).lower() or "no running event loop" in str(e).lower():
pass pass # Expected during shutdown
else:
print(f"WARNING: Unexpected error during cleanup: {e}", file=sys.stderr)
except Exception as e: except Exception as e:
print(f"Server failed to start: {e}") print(f"Server failed to start: {e}")
try: try:
asyncio.run(shutdown_cleanup()) asyncio.run(shutdown_cleanup())
except RuntimeError: except RuntimeError as e:
# Event loop already closed, skip cleanup if "event loop is closed" in str(e).lower() or "no running event loop" in str(e).lower():
pass pass # Expected during shutdown
else:
print(f"WARNING: Unexpected error during cleanup: {e}", file=sys.stderr)
raise raise

View File

@@ -29,6 +29,7 @@ from .tools.storage import register_storage_tool
from .tools.users import register_users_tool from .tools.users import register_users_tool
from .tools.virtualization import register_vm_tool from .tools.virtualization import register_vm_tool
# Initialize FastMCP instance # Initialize FastMCP instance
mcp = FastMCP( mcp = FastMCP(
name="Unraid MCP Server", name="Unraid MCP Server",
@@ -48,16 +49,20 @@ def register_all_modules() -> None:
logger.info("Subscription resources registered") logger.info("Subscription resources registered")
# Register all 10 consolidated tools # Register all 10 consolidated tools
register_info_tool(mcp) registrars = [
register_array_tool(mcp) register_info_tool,
register_storage_tool(mcp) register_array_tool,
register_docker_tool(mcp) register_storage_tool,
register_vm_tool(mcp) register_docker_tool,
register_notifications_tool(mcp) register_vm_tool,
register_rclone_tool(mcp) register_notifications_tool,
register_users_tool(mcp) register_rclone_tool,
register_keys_tool(mcp) register_users_tool,
register_health_tool(mcp) register_keys_tool,
register_health_tool,
]
for registrar in registrars:
registrar(mcp)
logger.info("All 10 tools registered successfully - Server ready!") logger.info("All 10 tools registered successfully - Server ready!")

View File

@@ -7,7 +7,6 @@ development and debugging purposes.
import asyncio import asyncio
import json import json
import ssl
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -16,10 +15,11 @@ from fastmcp import FastMCP
from websockets.typing import Subprotocol from websockets.typing import Subprotocol
from ..config.logging import logger from ..config.logging import logger
from ..config.settings import UNRAID_API_KEY, UNRAID_API_URL, UNRAID_VERIFY_SSL from ..config.settings import UNRAID_API_KEY, UNRAID_API_URL
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
from .manager import subscription_manager from .manager import subscription_manager
from .resources import ensure_subscriptions_started from .resources import ensure_subscriptions_started
from .utils import build_ws_ssl_context
def register_diagnostic_tools(mcp: FastMCP) -> None: def register_diagnostic_tools(mcp: FastMCP) -> None:
@@ -31,8 +31,8 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
@mcp.tool() @mcp.tool()
async def test_subscription_query(subscription_query: str) -> dict[str, Any]: async def test_subscription_query(subscription_query: str) -> dict[str, Any]:
""" """Test a GraphQL subscription query directly to debug schema issues.
Test a GraphQL subscription query directly to debug schema issues.
Use this to find working subscription field names and structure. Use this to find working subscription field names and structure.
Args: Args:
@@ -49,15 +49,7 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
raise ToolError("UNRAID_API_URL is not configured") raise ToolError("UNRAID_API_URL is not configured")
ws_url = UNRAID_API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/graphql" ws_url = UNRAID_API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/graphql"
# Build SSL context for wss:// connections ssl_context = build_ws_ssl_context(ws_url)
ssl_context = None
if ws_url.startswith("wss://"):
if isinstance(UNRAID_VERIFY_SSL, str):
ssl_context = ssl.create_default_context(cafile=UNRAID_VERIFY_SSL)
elif UNRAID_VERIFY_SSL:
ssl_context = ssl.create_default_context()
else:
ssl_context = ssl._create_unverified_context()
# Test connection # Test connection
async with websockets.connect( async with websockets.connect(
@@ -104,7 +96,7 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
"query_tested": subscription_query "query_tested": subscription_query
} }
except asyncio.TimeoutError: except TimeoutError:
return { return {
"success": True, "success": True,
"response": "No immediate response (subscriptions may only send data on changes)", "response": "No immediate response (subscriptions may only send data on changes)",
@@ -121,8 +113,8 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
@mcp.tool() @mcp.tool()
async def diagnose_subscriptions() -> dict[str, Any]: async def diagnose_subscriptions() -> dict[str, Any]:
""" """Comprehensive diagnostic tool for subscription system.
Comprehensive diagnostic tool for subscription system.
Shows detailed status, connection states, errors, and troubleshooting info. Shows detailed status, connection states, errors, and troubleshooting info.
Returns: Returns:
@@ -163,14 +155,14 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
# Calculate WebSocket URL # Calculate WebSocket URL
if UNRAID_API_URL: if UNRAID_API_URL:
if UNRAID_API_URL.startswith('https://'): if UNRAID_API_URL.startswith("https://"):
ws_url = 'wss://' + UNRAID_API_URL[len('https://'):] ws_url = "wss://" + UNRAID_API_URL[len("https://"):]
elif UNRAID_API_URL.startswith('http://'): elif UNRAID_API_URL.startswith("http://"):
ws_url = 'ws://' + UNRAID_API_URL[len('http://'):] ws_url = "ws://" + UNRAID_API_URL[len("http://"):]
else: else:
ws_url = UNRAID_API_URL ws_url = UNRAID_API_URL
if not ws_url.endswith('/graphql'): if not ws_url.endswith("/graphql"):
ws_url = ws_url.rstrip('/') + '/graphql' ws_url = ws_url.rstrip("/") + "/graphql"
diagnostic_info["environment"]["websocket_url"] = ws_url diagnostic_info["environment"]["websocket_url"] = ws_url
# Analyze issues # Analyze issues
@@ -222,6 +214,6 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
except Exception as e: except Exception as e:
logger.error(f"[DIAGNOSTIC] Failed to generate diagnostics: {e}") logger.error(f"[DIAGNOSTIC] Failed to generate diagnostics: {e}")
raise ToolError(f"Failed to generate diagnostics: {str(e)}") from e raise ToolError(f"Failed to generate diagnostics: {e!s}") from e
logger.info("Subscription diagnostic tools registered successfully") logger.info("Subscription diagnostic tools registered successfully")

View File

@@ -8,7 +8,6 @@ error handling, reconnection logic, and authentication.
import asyncio import asyncio
import json import json
import os import os
import ssl
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -16,8 +15,9 @@ import websockets
from websockets.typing import Subprotocol from websockets.typing import Subprotocol
from ..config.logging import logger from ..config.logging import logger
from ..config.settings import UNRAID_API_KEY, UNRAID_API_URL, UNRAID_VERIFY_SSL from ..config.settings import UNRAID_API_KEY, UNRAID_API_URL
from ..core.types import SubscriptionData from ..core.types import SubscriptionData
from .utils import build_ws_ssl_context
class SubscriptionManager: class SubscriptionManager:
@@ -141,28 +141,20 @@ class SubscriptionManager:
if not UNRAID_API_URL: if not UNRAID_API_URL:
raise ValueError("UNRAID_API_URL is not configured") raise ValueError("UNRAID_API_URL is not configured")
if UNRAID_API_URL.startswith('https://'): if UNRAID_API_URL.startswith("https://"):
ws_url = 'wss://' + UNRAID_API_URL[len('https://'):] ws_url = "wss://" + UNRAID_API_URL[len("https://"):]
elif UNRAID_API_URL.startswith('http://'): elif UNRAID_API_URL.startswith("http://"):
ws_url = 'ws://' + UNRAID_API_URL[len('http://'):] ws_url = "ws://" + UNRAID_API_URL[len("http://"):]
else: else:
ws_url = UNRAID_API_URL ws_url = UNRAID_API_URL
if not ws_url.endswith('/graphql'): if not ws_url.endswith("/graphql"):
ws_url = ws_url.rstrip('/') + '/graphql' ws_url = ws_url.rstrip("/") + "/graphql"
logger.debug(f"[WEBSOCKET:{subscription_name}] Connecting to: {ws_url}") logger.debug(f"[WEBSOCKET:{subscription_name}] Connecting to: {ws_url}")
logger.debug(f"[WEBSOCKET:{subscription_name}] API Key present: {'Yes' if UNRAID_API_KEY else 'No'}") logger.debug(f"[WEBSOCKET:{subscription_name}] API Key present: {'Yes' if UNRAID_API_KEY else 'No'}")
# Build SSL context for wss:// connections ssl_context = build_ws_ssl_context(ws_url)
ssl_context = None
if ws_url.startswith('wss://'):
if isinstance(UNRAID_VERIFY_SSL, str):
ssl_context = ssl.create_default_context(cafile=UNRAID_VERIFY_SSL)
elif UNRAID_VERIFY_SSL:
ssl_context = ssl.create_default_context()
else:
ssl_context = ssl._create_unverified_context()
# Connection with timeout # Connection with timeout
connect_timeout = 10 connect_timeout = 10
@@ -213,7 +205,7 @@ class SubscriptionManager:
init_data = json.loads(init_raw) init_data = json.loads(init_raw)
logger.debug(f"[PROTOCOL:{subscription_name}] Received init response: {init_data.get('type')}") logger.debug(f"[PROTOCOL:{subscription_name}] Received init response: {init_data.get('type')}")
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
init_preview = init_raw[:200] if isinstance(init_raw, str) else init_raw[:200].decode('utf-8', errors='replace') init_preview = init_raw[:200] if isinstance(init_raw, str) else init_raw[:200].decode("utf-8", errors="replace")
logger.error(f"[PROTOCOL:{subscription_name}] Failed to decode init response: {init_preview}...") logger.error(f"[PROTOCOL:{subscription_name}] Failed to decode init response: {init_preview}...")
self.last_error[subscription_name] = f"Invalid JSON in init response: {e}" self.last_error[subscription_name] = f"Invalid JSON in init response: {e}"
break break
@@ -223,7 +215,7 @@ class SubscriptionManager:
logger.info(f"[PROTOCOL:{subscription_name}] Connection acknowledged successfully") logger.info(f"[PROTOCOL:{subscription_name}] Connection acknowledged successfully")
self.connection_states[subscription_name] = "authenticated" self.connection_states[subscription_name] = "authenticated"
elif init_data.get("type") == "connection_error": elif init_data.get("type") == "connection_error":
error_payload = init_data.get('payload', {}) error_payload = init_data.get("payload", {})
logger.error(f"[AUTH:{subscription_name}] Authentication failed: {error_payload}") logger.error(f"[AUTH:{subscription_name}] Authentication failed: {error_payload}")
self.last_error[subscription_name] = f"Authentication error: {error_payload}" self.last_error[subscription_name] = f"Authentication error: {error_payload}"
self.connection_states[subscription_name] = "auth_failed" self.connection_states[subscription_name] = "auth_failed"
@@ -259,7 +251,7 @@ class SubscriptionManager:
try: try:
data = json.loads(message) data = json.loads(message)
message_count += 1 message_count += 1
message_type = data.get('type', 'unknown') message_type = data.get("type", "unknown")
logger.debug(f"[DATA:{subscription_name}] Message #{message_count}: {message_type}") logger.debug(f"[DATA:{subscription_name}] Message #{message_count}: {message_type}")
@@ -288,7 +280,7 @@ class SubscriptionManager:
await websocket.send(json.dumps({"type": "pong"})) await websocket.send(json.dumps({"type": "pong"}))
elif data.get("type") == "error": elif data.get("type") == "error":
error_payload = data.get('payload', {}) error_payload = data.get("payload", {})
logger.error(f"[SUBSCRIPTION:{subscription_name}] Subscription error: {error_payload}") logger.error(f"[SUBSCRIPTION:{subscription_name}] Subscription error: {error_payload}")
self.last_error[subscription_name] = f"Subscription error: {error_payload}" self.last_error[subscription_name] = f"Subscription error: {error_payload}"
self.connection_states[subscription_name] = "error" self.connection_states[subscription_name] = "error"
@@ -305,15 +297,15 @@ class SubscriptionManager:
logger.debug(f"[PROTOCOL:{subscription_name}] Unhandled message type: {message_type}") logger.debug(f"[PROTOCOL:{subscription_name}] Unhandled message type: {message_type}")
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
msg_preview = message[:200] if isinstance(message, str) else message[:200].decode('utf-8', errors='replace') msg_preview = message[:200] if isinstance(message, str) else message[:200].decode("utf-8", errors="replace")
logger.error(f"[PROTOCOL:{subscription_name}] Failed to decode message: {msg_preview}...") logger.error(f"[PROTOCOL:{subscription_name}] Failed to decode message: {msg_preview}...")
logger.error(f"[PROTOCOL:{subscription_name}] JSON decode error: {e}") logger.error(f"[PROTOCOL:{subscription_name}] JSON decode error: {e}")
except Exception as e: except Exception as e:
logger.error(f"[DATA:{subscription_name}] Error processing message: {e}") logger.error(f"[DATA:{subscription_name}] Error processing message: {e}")
msg_preview = message[:200] if isinstance(message, str) else message[:200].decode('utf-8', errors='replace') msg_preview = message[:200] if isinstance(message, str) else message[:200].decode("utf-8", errors="replace")
logger.debug(f"[DATA:{subscription_name}] Raw message: {msg_preview}...") logger.debug(f"[DATA:{subscription_name}] Raw message: {msg_preview}...")
except asyncio.TimeoutError: except TimeoutError:
error_msg = "Connection or authentication timeout" error_msg = "Connection or authentication timeout"
logger.error(f"[WEBSOCKET:{subscription_name}] {error_msg}") logger.error(f"[WEBSOCKET:{subscription_name}] {error_msg}")
self.last_error[subscription_name] = error_msg self.last_error[subscription_name] = error_msg
@@ -353,7 +345,6 @@ class SubscriptionManager:
age_seconds = (datetime.now() - data.last_updated).total_seconds() age_seconds = (datetime.now() - data.last_updated).total_seconds()
logger.debug(f"[RESOURCE:{resource_name}] Data found, age: {age_seconds:.1f}s") logger.debug(f"[RESOURCE:{resource_name}] Data found, age: {age_seconds:.1f}s")
return data.data return data.data
else:
logger.debug(f"[RESOURCE:{resource_name}] No data available") logger.debug(f"[RESOURCE:{resource_name}] No data available")
return None return None

View File

@@ -13,6 +13,7 @@ from fastmcp import FastMCP
from ..config.logging import logger from ..config.logging import logger
from .manager import subscription_manager from .manager import subscription_manager
# Global flag to track subscription startup # Global flag to track subscription startup
_subscriptions_started = False _subscriptions_started = False

View File

@@ -0,0 +1,27 @@
"""Shared utilities for the subscription system."""
import ssl as _ssl
from ..config.settings import UNRAID_VERIFY_SSL
def build_ws_ssl_context(ws_url: str) -> _ssl.SSLContext | None:
"""Build an SSL context for WebSocket connections when using wss://.
Args:
ws_url: The WebSocket URL to connect to.
Returns:
An SSLContext configured per UNRAID_VERIFY_SSL, or None for non-TLS URLs.
"""
if not ws_url.startswith("wss://"):
return None
if isinstance(UNRAID_VERIFY_SSL, str):
return _ssl.create_default_context(cafile=UNRAID_VERIFY_SSL)
if UNRAID_VERIFY_SSL:
return _ssl.create_default_context()
# Explicitly disable verification (equivalent to verify=False)
ctx = _ssl.SSLContext(_ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = _ssl.CERT_NONE
return ctx

View File

@@ -12,9 +12,10 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"parity_history": """ "parity_status": """
query GetParityHistory { query GetParityStatus {
array { parityCheckStatus { progress speed errors } } array { parityCheckStatus { progress speed errors } }
} }
""", """,
@@ -80,10 +81,11 @@ MUTATIONS: dict[str, str] = {
DESTRUCTIVE_ACTIONS = {"start", "stop", "shutdown", "reboot"} DESTRUCTIVE_ACTIONS = {"start", "stop", "shutdown", "reboot"}
DISK_ACTIONS = {"mount_disk", "unmount_disk", "clear_stats"} DISK_ACTIONS = {"mount_disk", "unmount_disk", "clear_stats"}
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
ARRAY_ACTIONS = Literal[ ARRAY_ACTIONS = Literal[
"start", "stop", "start", "stop",
"parity_start", "parity_pause", "parity_resume", "parity_cancel", "parity_history", "parity_start", "parity_pause", "parity_resume", "parity_cancel", "parity_status",
"mount_disk", "unmount_disk", "clear_stats", "mount_disk", "unmount_disk", "clear_stats",
"shutdown", "reboot", "shutdown", "reboot",
] ]
@@ -108,16 +110,15 @@ def register_array_tool(mcp: FastMCP) -> None:
parity_pause - Pause running parity check parity_pause - Pause running parity check
parity_resume - Resume paused parity check parity_resume - Resume paused parity check
parity_cancel - Cancel running parity check parity_cancel - Cancel running parity check
parity_history - Get parity check status/history parity_status - Get current parity check status
mount_disk - Mount an array disk (requires disk_id) mount_disk - Mount an array disk (requires disk_id)
unmount_disk - Unmount an array disk (requires disk_id) unmount_disk - Unmount an array disk (requires disk_id)
clear_stats - Clear disk statistics (requires disk_id) clear_stats - Clear disk statistics (requires disk_id)
shutdown - Shut down the server (destructive, requires confirm=True) shutdown - Shut down the server (destructive, requires confirm=True)
reboot - Reboot the server (destructive, requires confirm=True) reboot - Reboot the server (destructive, requires confirm=True)
""" """
all_actions = set(QUERIES) | set(MUTATIONS) if action not in ALL_ACTIONS:
if action not in all_actions: raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
if action in DESTRUCTIVE_ACTIONS and not confirm: if action in DESTRUCTIVE_ACTIONS and not confirm:
raise ToolError( raise ToolError(
@@ -156,6 +157,6 @@ def register_array_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_array action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_array action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute array/{action}: {str(e)}") from e raise ToolError(f"Failed to execute array/{action}: {e!s}") from e
logger.info("Array tool registered successfully") logger.info("Array tool registered successfully")

View File

@@ -13,6 +13,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"list": """ "list": """
query ListDockerContainers { query ListDockerContainers {
@@ -98,7 +99,8 @@ MUTATIONS: dict[str, str] = {
} }
DESTRUCTIVE_ACTIONS = {"remove"} DESTRUCTIVE_ACTIONS = {"remove"}
CONTAINER_ACTIONS = {"start", "stop", "restart", "pause", "unpause", "remove", "update", "details", "logs"} _ACTIONS_REQUIRING_CONTAINER_ID = {"start", "stop", "restart", "pause", "unpause", "remove", "update", "details", "logs"}
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS) | {"restart"}
DOCKER_ACTIONS = Literal[ DOCKER_ACTIONS = Literal[
"list", "details", "start", "stop", "restart", "pause", "unpause", "list", "details", "start", "stop", "restart", "pause", "unpause",
@@ -107,7 +109,7 @@ DOCKER_ACTIONS = Literal[
] ]
# Docker container IDs: 64 hex chars + optional suffix (e.g., ":local") # Docker container IDs: 64 hex chars + optional suffix (e.g., ":local")
_DOCKER_ID_PATTERN = re.compile(r"^[a-f0-9]{64}(:[a-z0-9]+)?$") _DOCKER_ID_PATTERN = re.compile(r"^[a-f0-9]{64}(:[a-z0-9]+)?$", re.IGNORECASE)
def find_container_by_identifier( def find_container_by_identifier(
@@ -175,6 +177,7 @@ def register_docker_tool(mcp: FastMCP) -> None:
action: DOCKER_ACTIONS, action: DOCKER_ACTIONS,
container_id: str | None = None, container_id: str | None = None,
network_id: str | None = None, network_id: str | None = None,
*,
confirm: bool = False, confirm: bool = False,
tail_lines: int = 100, tail_lines: int = 100,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -197,14 +200,13 @@ def register_docker_tool(mcp: FastMCP) -> None:
port_conflicts - Check for port conflicts port_conflicts - Check for port conflicts
check_updates - Check which containers have updates available check_updates - Check which containers have updates available
""" """
all_actions = set(QUERIES) | set(MUTATIONS) | {"restart"} if action not in ALL_ACTIONS:
if action not in all_actions: raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
if action in DESTRUCTIVE_ACTIONS and not confirm: if action in DESTRUCTIVE_ACTIONS and not confirm:
raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.") raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.")
if action in CONTAINER_ACTIONS and not container_id: if action in _ACTIONS_REQUIRING_CONTAINER_ID and not container_id:
raise ToolError(f"container_id is required for '{action}' action") raise ToolError(f"container_id is required for '{action}' action")
if action == "network_details" and not network_id: if action == "network_details" and not network_id:
@@ -327,6 +329,6 @@ def register_docker_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_docker action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_docker action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute docker/{action}: {str(e)}") from e raise ToolError(f"Failed to execute docker/{action}: {e!s}") from e
logger.info("Docker tool registered successfully") logger.info("Docker tool registered successfully")

View File

@@ -21,12 +21,24 @@ from ..config.settings import (
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
HEALTH_ACTIONS = Literal["check", "test_connection", "diagnose"] HEALTH_ACTIONS = Literal["check", "test_connection", "diagnose"]
# Severity ordering: only upgrade, never downgrade # Severity ordering: only upgrade, never downgrade
_SEVERITY = {"healthy": 0, "warning": 1, "degraded": 2, "unhealthy": 3} _SEVERITY = {"healthy": 0, "warning": 1, "degraded": 2, "unhealthy": 3}
def _server_info() -> dict[str, Any]:
"""Return the standard server info block used in health responses."""
return {
"name": "Unraid MCP Server",
"version": VERSION,
"transport": UNRAID_MCP_TRANSPORT,
"host": UNRAID_MCP_HOST,
"port": UNRAID_MCP_PORT,
}
def register_health_tool(mcp: FastMCP) -> None: def register_health_tool(mcp: FastMCP) -> None:
"""Register the unraid_health tool with the FastMCP instance.""" """Register the unraid_health tool with the FastMCP instance."""
@@ -71,7 +83,7 @@ def register_health_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_health action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_health action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute health/{action}: {str(e)}") from e raise ToolError(f"Failed to execute health/{action}: {e!s}") from e
logger.info("Health tool registered successfully") logger.info("Health tool registered successfully")
@@ -108,15 +120,9 @@ async def _comprehensive_check() -> dict[str, Any]:
health_info: dict[str, Any] = { health_info: dict[str, Any] = {
"status": "healthy", "status": "healthy",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
"api_latency_ms": api_latency, "api_latency_ms": api_latency,
"server": { "server": _server_info(),
"name": "Unraid MCP Server",
"version": VERSION,
"transport": UNRAID_MCP_TRANSPORT,
"host": UNRAID_MCP_HOST,
"port": UNRAID_MCP_PORT,
},
} }
if not data: if not data:
@@ -201,15 +207,9 @@ async def _comprehensive_check() -> dict[str, Any]:
logger.error(f"Health check failed: {e}") logger.error(f"Health check failed: {e}")
return { return {
"status": "unhealthy", "status": "unhealthy",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
"error": str(e), "error": str(e),
"server": { "server": _server_info(),
"name": "Unraid MCP Server",
"version": VERSION,
"transport": UNRAID_MCP_TRANSPORT,
"host": UNRAID_MCP_HOST,
"port": UNRAID_MCP_PORT,
},
} }
@@ -225,7 +225,7 @@ async def _diagnose_subscriptions() -> dict[str, Any]:
connection_issues: list[dict[str, Any]] = [] connection_issues: list[dict[str, Any]] = []
diagnostic_info: dict[str, Any] = { diagnostic_info: dict[str, Any] = {
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
"environment": { "environment": {
"auto_start_enabled": subscription_manager.auto_start_enabled, "auto_start_enabled": subscription_manager.auto_start_enabled,
"max_reconnect_attempts": subscription_manager.max_reconnect_attempts, "max_reconnect_attempts": subscription_manager.max_reconnect_attempts,
@@ -258,7 +258,7 @@ async def _diagnose_subscriptions() -> dict[str, Any]:
except ImportError: except ImportError:
return { return {
"error": "Subscription modules not available", "error": "Subscription modules not available",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
} }
except Exception as e: except Exception as e:
raise ToolError(f"Failed to generate diagnostics: {str(e)}") from e raise ToolError(f"Failed to generate diagnostics: {e!s}") from e

View File

@@ -12,6 +12,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
# Pre-built queries keyed by action name # Pre-built queries keyed by action name
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"overview": """ "overview": """
@@ -162,6 +163,10 @@ INFO_ACTIONS = Literal[
"ups_devices", "ups_device", "ups_config", "ups_devices", "ups_device", "ups_config",
] ]
assert set(QUERIES.keys()) == set(INFO_ACTIONS.__args__), (
"QUERIES keys and INFO_ACTIONS are out of sync"
)
def _process_system_info(raw_info: dict[str, Any]) -> dict[str, Any]: def _process_system_info(raw_info: dict[str, Any]) -> dict[str, Any]:
"""Process raw system info into summary + details.""" """Process raw system info into summary + details."""
@@ -179,7 +184,7 @@ def _process_system_info(raw_info: dict[str, Any]) -> dict[str, Any]:
cpu = raw_info["cpu"] cpu = raw_info["cpu"]
summary["cpu"] = ( summary["cpu"] = (
f"{cpu.get('manufacturer', '')} {cpu.get('brand', '')} " f"{cpu.get('manufacturer', '')} {cpu.get('brand', '')} "
f"({cpu.get('cores')} cores, {cpu.get('threads')} threads)" f"({cpu.get('cores', '?')} cores, {cpu.get('threads', '?')} threads)"
) )
if raw_info.get("memory") and raw_info["memory"].get("layout"): if raw_info.get("memory") and raw_info["memory"].get("layout"):
@@ -227,13 +232,14 @@ def _analyze_disk_health(disks: list[dict[str, Any]]) -> dict[str, int]:
return counts return counts
def _process_array_status(raw: dict[str, Any]) -> dict[str, Any]: def _format_kb(k: Any) -> str:
"""Process raw array data into summary + details.""" """Format kilobyte values into human-readable sizes."""
def format_kb(k: Any) -> str:
if k is None: if k is None:
return "N/A" return "N/A"
try:
k = int(k) k = int(k)
except (ValueError, TypeError):
return "N/A"
if k >= 1024 * 1024 * 1024: if k >= 1024 * 1024 * 1024:
return f"{k / (1024 * 1024 * 1024):.2f} TB" return f"{k / (1024 * 1024 * 1024):.2f} TB"
if k >= 1024 * 1024: if k >= 1024 * 1024:
@@ -242,12 +248,15 @@ def _process_array_status(raw: dict[str, Any]) -> dict[str, Any]:
return f"{k / 1024:.2f} MB" return f"{k / 1024:.2f} MB"
return f"{k} KB" return f"{k} KB"
def _process_array_status(raw: dict[str, Any]) -> dict[str, Any]:
"""Process raw array data into summary + details."""
summary: dict[str, Any] = {"state": raw.get("state")} summary: dict[str, Any] = {"state": raw.get("state")}
if raw.get("capacity") and raw["capacity"].get("kilobytes"): if raw.get("capacity") and raw["capacity"].get("kilobytes"):
kb = raw["capacity"]["kilobytes"] kb = raw["capacity"]["kilobytes"]
summary["capacity_total"] = format_kb(kb.get("total")) summary["capacity_total"] = _format_kb(kb.get("total"))
summary["capacity_used"] = format_kb(kb.get("used")) summary["capacity_used"] = _format_kb(kb.get("used"))
summary["capacity_free"] = format_kb(kb.get("free")) summary["capacity_free"] = _format_kb(kb.get("free"))
summary["num_parity_disks"] = len(raw.get("parities", [])) summary["num_parity_disks"] = len(raw.get("parities", []))
summary["num_data_disks"] = len(raw.get("disks", [])) summary["num_data_disks"] = len(raw.get("disks", []))
@@ -320,81 +329,73 @@ def register_info_tool(mcp: FastMCP) -> None:
if action == "ups_device": if action == "ups_device":
variables = {"id": device_id} variables = {"id": device_id}
# Lookup tables for common response patterns
# Simple dict actions: action -> GraphQL response key
dict_actions: dict[str, str] = {
"network": "network",
"registration": "registration",
"connect": "connect",
"variables": "vars",
"metrics": "metrics",
"config": "config",
"owner": "owner",
"flash": "flash",
"ups_device": "upsDeviceById",
"ups_config": "upsConfiguration",
}
# List-wrapped actions: action -> (GraphQL response key, output key)
list_actions: dict[str, tuple[str, str]] = {
"services": ("services", "services"),
"servers": ("servers", "servers"),
"ups_devices": ("upsDevices", "ups_devices"),
}
try: try:
logger.info(f"Executing unraid_info action={action}") logger.info(f"Executing unraid_info action={action}")
data = await make_graphql_request(query, variables) data = await make_graphql_request(query, variables)
# Action-specific response processing # Special-case actions with custom processing
if action == "overview": if action == "overview":
raw = data.get("info", {}) raw = data.get("info") or {}
if not raw: if not raw:
raise ToolError("No system info returned from Unraid API") raise ToolError("No system info returned from Unraid API")
return _process_system_info(raw) return _process_system_info(raw)
if action == "array": if action == "array":
raw = data.get("array", {}) raw = data.get("array") or {}
if not raw: if not raw:
raise ToolError("No array information returned from Unraid API") raise ToolError("No array information returned from Unraid API")
return _process_array_status(raw) return _process_array_status(raw)
if action == "network":
return dict(data.get("network", {}))
if action == "registration":
return dict(data.get("registration", {}))
if action == "connect":
return dict(data.get("connect", {}))
if action == "variables":
return dict(data.get("vars", {}))
if action == "metrics":
return dict(data.get("metrics", {}))
if action == "services":
services = data.get("services", [])
return {"services": list(services) if isinstance(services, list) else []}
if action == "display": if action == "display":
info = data.get("info", {}) info = data.get("info") or {}
return dict(info.get("display", {})) return dict(info.get("display") or {})
if action == "config":
return dict(data.get("config", {}))
if action == "online": if action == "online":
return {"online": data.get("online")} return {"online": data.get("online")}
if action == "owner":
return dict(data.get("owner", {}))
if action == "settings": if action == "settings":
settings = data.get("settings", {}) settings = data.get("settings") or {}
if settings and settings.get("unified"): if not settings:
values = settings["unified"].get("values", {}) raise ToolError("No settings data returned from Unraid API. Check API permissions.")
if not settings.get("unified"):
logger.warning(f"Settings returned unexpected structure: {settings.keys()}")
raise ToolError(f"Unexpected settings structure. Expected 'unified' key, got: {list(settings.keys())}")
values = settings["unified"].get("values") or {}
return dict(values) if isinstance(values, dict) else {"raw": values} return dict(values) if isinstance(values, dict) else {"raw": values}
return {}
if action == "server": if action == "server":
return data return data
if action == "servers": # Simple dict-returning actions
servers = data.get("servers", []) if action in dict_actions:
return {"servers": list(servers) if isinstance(servers, list) else []} return dict(data.get(dict_actions[action]) or {})
if action == "flash": # List-wrapped actions
return dict(data.get("flash", {})) if action in list_actions:
response_key, output_key = list_actions[action]
if action == "ups_devices": items = data.get(response_key) or []
devices = data.get("upsDevices", []) return {output_key: list(items) if isinstance(items, list) else []}
return {"ups_devices": list(devices) if isinstance(devices, list) else []}
if action == "ups_device":
return dict(data.get("upsDeviceById", {}))
if action == "ups_config":
return dict(data.get("upsConfiguration", {}))
raise ToolError(f"Unhandled action '{action}' — this is a bug") raise ToolError(f"Unhandled action '{action}' — this is a bug")
@@ -402,6 +403,6 @@ def register_info_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_info action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_info action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute info/{action}: {str(e)}") from e raise ToolError(f"Failed to execute info/{action}: {e!s}") from e
logger.info("Info tool registered successfully") logger.info("Info tool registered successfully")

View File

@@ -12,6 +12,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"list": """ "list": """
query ListApiKeys { query ListApiKeys {
@@ -90,7 +91,7 @@ def register_keys_tool(mcp: FastMCP) -> None:
if not key_id: if not key_id:
raise ToolError("key_id is required for 'get' action") raise ToolError("key_id is required for 'get' action")
data = await make_graphql_request(QUERIES["get"], {"id": key_id}) data = await make_graphql_request(QUERIES["get"], {"id": key_id})
return dict(data.get("apiKey", {})) return dict(data.get("apiKey") or {})
if action == "create": if action == "create":
if not name: if not name:
@@ -144,6 +145,6 @@ def register_keys_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_keys action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_keys action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute keys/{action}: {str(e)}") from e raise ToolError(f"Failed to execute keys/{action}: {e!s}") from e
logger.info("Keys tool registered successfully") logger.info("Keys tool registered successfully")

View File

@@ -12,6 +12,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"overview": """ "overview": """
query GetNotificationsOverview { query GetNotificationsOverview {
@@ -124,8 +125,8 @@ def register_notifications_tool(mcp: FastMCP) -> None:
if action == "overview": if action == "overview":
data = await make_graphql_request(QUERIES["overview"]) data = await make_graphql_request(QUERIES["overview"])
notifications = data.get("notifications", {}) notifications = data.get("notifications") or {}
return dict(notifications.get("overview", {})) return dict(notifications.get("overview") or {})
if action == "list": if action == "list":
filter_vars: dict[str, Any] = { filter_vars: dict[str, Any] = {
@@ -200,6 +201,6 @@ def register_notifications_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_notifications action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_notifications action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute notifications/{action}: {str(e)}") from e raise ToolError(f"Failed to execute notifications/{action}: {e!s}") from e
logger.info("Notifications tool registered successfully") logger.info("Notifications tool registered successfully")

View File

@@ -12,6 +12,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"list_remotes": """ "list_remotes": """
query ListRCloneRemotes { query ListRCloneRemotes {
@@ -39,6 +40,7 @@ MUTATIONS: dict[str, str] = {
} }
DESTRUCTIVE_ACTIONS = {"delete_remote"} DESTRUCTIVE_ACTIONS = {"delete_remote"}
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
RCLONE_ACTIONS = Literal[ RCLONE_ACTIONS = Literal[
"list_remotes", "config_form", "create_remote", "delete_remote", "list_remotes", "config_form", "create_remote", "delete_remote",
@@ -64,9 +66,8 @@ def register_rclone_tool(mcp: FastMCP) -> None:
create_remote - Create a new remote (requires name, provider_type, config_data) create_remote - Create a new remote (requires name, provider_type, config_data)
delete_remote - Delete a remote (requires name, confirm=True) delete_remote - Delete a remote (requires name, confirm=True)
""" """
all_actions = set(QUERIES) | set(MUTATIONS) if action not in ALL_ACTIONS:
if action not in all_actions: raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
if action in DESTRUCTIVE_ACTIONS and not confirm: if action in DESTRUCTIVE_ACTIONS and not confirm:
raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.") raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.")
@@ -129,6 +130,6 @@ def register_rclone_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_rclone action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_rclone action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute rclone/{action}: {str(e)}") from e raise ToolError(f"Failed to execute rclone/{action}: {e!s}") from e
logger.info("RClone tool registered successfully") logger.info("RClone tool registered successfully")

View File

@@ -4,7 +4,7 @@ Provides the `unraid_storage` tool with 6 actions for shares, physical disks,
unassigned devices, log files, and log content retrieval. unassigned devices, log files, and log content retrieval.
""" """
import posixpath from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
from fastmcp import FastMCP from fastmcp import FastMCP
@@ -102,8 +102,8 @@ def register_storage_tool(mcp: FastMCP) -> None:
if action == "logs": if action == "logs":
if not log_path: if not log_path:
raise ToolError("log_path is required for 'logs' action") raise ToolError("log_path is required for 'logs' action")
# Normalize path to prevent traversal attacks (e.g. /var/log/../../etc/shadow) # Resolve path to prevent traversal attacks (e.g. /var/log/../../etc/shadow)
normalized = posixpath.normpath(log_path) normalized = str(Path(log_path).resolve())
if not any(normalized.startswith(p) for p in _ALLOWED_LOG_PREFIXES): if not any(normalized.startswith(p) for p in _ALLOWED_LOG_PREFIXES):
raise ToolError( raise ToolError(
f"log_path must start with one of: {', '.join(_ALLOWED_LOG_PREFIXES)}. " f"log_path must start with one of: {', '.join(_ALLOWED_LOG_PREFIXES)}. "
@@ -143,8 +143,8 @@ def register_storage_tool(mcp: FastMCP) -> None:
"serial_number": raw.get("serialNum"), "serial_number": raw.get("serialNum"),
"size_formatted": format_bytes(raw.get("size")), "size_formatted": format_bytes(raw.get("size")),
"temperature": ( "temperature": (
f"{raw.get('temperature')}C" f"{raw['temperature']}\u00b0C"
if raw.get("temperature") if raw.get("temperature") is not None
else "N/A" else "N/A"
), ),
} }
@@ -159,7 +159,7 @@ def register_storage_tool(mcp: FastMCP) -> None:
return {"log_files": list(files) if isinstance(files, list) else []} return {"log_files": list(files) if isinstance(files, list) else []}
if action == "logs": if action == "logs":
return dict(data.get("logFile", {})) return dict(data.get("logFile") or {})
raise ToolError(f"Unhandled action '{action}' — this is a bug") raise ToolError(f"Unhandled action '{action}' — this is a bug")

View File

@@ -12,6 +12,7 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"me": """ "me": """
query GetMe { query GetMe {
@@ -158,6 +159,6 @@ def register_users_tool(mcp: FastMCP) -> None:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error in unraid_users action={action}: {e}", exc_info=True) logger.error(f"Error in unraid_users action={action}: {e}", exc_info=True)
raise ToolError(f"Failed to execute users/{action}: {str(e)}") from e raise ToolError(f"Failed to execute users/{action}: {e!s}") from e
logger.info("Users tool registered successfully") logger.info("Users tool registered successfully")

View File

@@ -12,17 +12,13 @@ from ..config.logging import logger
from ..core.client import make_graphql_request from ..core.client import make_graphql_request
from ..core.exceptions import ToolError from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"list": """ "list": """
query ListVMs { query ListVMs {
vms { id domains { id name state uuid } } vms { id domains { id name state uuid } }
} }
""", """,
"details": """
query GetVmDetails {
vms { domains { id name state uuid } }
}
""",
} }
MUTATIONS: dict[str, str] = { MUTATIONS: dict[str, str] = {
@@ -49,15 +45,9 @@ MUTATIONS: dict[str, str] = {
""", """,
} }
# Map action names to their GraphQL field names # Map action names to GraphQL field names (only where they differ)
_MUTATION_FIELDS: dict[str, str] = { _MUTATION_FIELDS: dict[str, str] = {
"start": "start",
"stop": "stop",
"pause": "pause",
"resume": "resume",
"force_stop": "forceStop", "force_stop": "forceStop",
"reboot": "reboot",
"reset": "reset",
} }
DESTRUCTIVE_ACTIONS = {"force_stop", "reset"} DESTRUCTIVE_ACTIONS = {"force_stop", "reset"}
@@ -90,7 +80,7 @@ def register_vm_tool(mcp: FastMCP) -> None:
reboot - Reboot a VM (requires vm_id) reboot - Reboot a VM (requires vm_id)
reset - Reset a VM (requires vm_id, confirm=True) reset - Reset a VM (requires vm_id, confirm=True)
""" """
all_actions = set(QUERIES) | set(MUTATIONS) all_actions = set(QUERIES) | set(MUTATIONS) | {"details"}
if action not in all_actions: if action not in all_actions:
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}") raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
@@ -103,39 +93,40 @@ def register_vm_tool(mcp: FastMCP) -> None:
try: try:
logger.info(f"Executing unraid_vm action={action}") logger.info(f"Executing unraid_vm action={action}")
if action == "list": if action in ("list", "details"):
data = await make_graphql_request(QUERIES["list"]) data = await make_graphql_request(QUERIES["list"])
if data.get("vms"):
vms = data["vms"].get("domains") or data["vms"].get("domain")
if vms:
return {"vms": list(vms) if isinstance(vms, list) else []}
return {"vms": []}
if action == "details":
data = await make_graphql_request(QUERIES["details"])
if data.get("vms"): if data.get("vms"):
vms = data["vms"].get("domains") or data["vms"].get("domain") or [] vms = data["vms"].get("domains") or data["vms"].get("domain") or []
if isinstance(vms, dict):
vms = [vms]
if action == "list":
return {"vms": vms}
# details: find specific VM
for vm in vms: for vm in vms:
if ( if (
vm.get("uuid") == vm_id vm.get("uuid") == vm_id
or vm.get("id") == vm_id or vm.get("id") == vm_id
or vm.get("name") == vm_id or vm.get("name") == vm_id
): ):
return dict(vm) if isinstance(vm, dict) else {} return dict(vm)
available = [ available = [
f"{v.get('name')} (UUID: {v.get('uuid')})" for v in vms f"{v.get('name')} (UUID: {v.get('uuid')})" for v in vms
] ]
raise ToolError( raise ToolError(
f"VM '{vm_id}' not found. Available: {', '.join(available)}" f"VM '{vm_id}' not found. Available: {', '.join(available)}"
) )
if action == "details":
raise ToolError("No VM data returned from server") raise ToolError("No VM data returned from server")
return {"vms": []}
# Mutations # Mutations
if action in MUTATIONS: if action in MUTATIONS:
data = await make_graphql_request( data = await make_graphql_request(
MUTATIONS[action], {"id": vm_id} MUTATIONS[action], {"id": vm_id}
) )
field = _MUTATION_FIELDS[action] field = _MUTATION_FIELDS.get(action, action)
if data.get("vm") and field in data["vm"]: if data.get("vm") and field in data["vm"]:
return { return {
"success": data["vm"][field], "success": data["vm"][field],

553
uv.lock generated
View File

@@ -1,6 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.10" requires-python = ">=3.12"
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@@ -16,7 +16,6 @@ name = "anyio"
version = "4.11.0" version = "4.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" }, { name = "idna" },
{ name = "sniffio" }, { name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" },
@@ -47,69 +46,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/aa/91355b5f539caf1b94f0e66ff1e4ee39373b757fce08204981f7829ede51/authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796", size = 243076, upload-time = "2025-09-17T09:59:22.259Z" }, { url = "https://files.pythonhosted.org/packages/0e/aa/91355b5f539caf1b94f0e66ff1e4ee39373b757fce08204981f7829ede51/authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796", size = 243076, upload-time = "2025-09-17T09:59:22.259Z" },
] ]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]]
name = "backports-tarfile"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
]
[[package]]
name = "black"
version = "25.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "pytokens" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/40/dbe31fc56b218a858c8fc6f5d8d3ba61c1fa7e989d43d4a4574b8b992840/black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7", size = 1715605, upload-time = "2025-09-19T00:36:13.483Z" },
{ url = "https://files.pythonhosted.org/packages/92/b2/f46800621200eab6479b1f4c0e3ede5b4c06b768e79ee228bc80270bcc74/black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92", size = 1571829, upload-time = "2025-09-19T00:32:42.13Z" },
{ url = "https://files.pythonhosted.org/packages/4e/64/5c7f66bd65af5c19b4ea86062bb585adc28d51d37babf70969e804dbd5c2/black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713", size = 1631888, upload-time = "2025-09-19T00:30:54.212Z" },
{ url = "https://files.pythonhosted.org/packages/3b/64/0b9e5bfcf67db25a6eef6d9be6726499a8a72ebab3888c2de135190853d3/black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1", size = 1327056, upload-time = "2025-09-19T00:31:08.877Z" },
{ url = "https://files.pythonhosted.org/packages/b7/f4/7531d4a336d2d4ac6cc101662184c8e7d068b548d35d874415ed9f4116ef/black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa", size = 1698727, upload-time = "2025-09-19T00:31:14.264Z" },
{ url = "https://files.pythonhosted.org/packages/28/f9/66f26bfbbf84b949cc77a41a43e138d83b109502cd9c52dfc94070ca51f2/black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d", size = 1555679, upload-time = "2025-09-19T00:31:29.265Z" },
{ url = "https://files.pythonhosted.org/packages/bf/59/61475115906052f415f518a648a9ac679d7afbc8da1c16f8fdf68a8cebed/black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608", size = 1617453, upload-time = "2025-09-19T00:30:42.24Z" },
{ url = "https://files.pythonhosted.org/packages/7f/5b/20fd5c884d14550c911e4fb1b0dae00d4abb60a4f3876b449c4d3a9141d5/black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f", size = 1333655, upload-time = "2025-09-19T00:30:56.715Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" },
{ url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" },
{ url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" },
{ url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" },
{ url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" },
{ url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" },
{ url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" },
{ url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" },
{ url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" },
]
[[package]] [[package]]
name = "build" name = "build"
version = "1.4.0" version = "1.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "os_name == 'nt'" }, { name = "colorama", marker = "os_name == 'nt'" },
{ name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pyproject-hooks" }, { name = "pyproject-hooks" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" }
wheels = [ wheels = [
@@ -134,31 +78,6 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
@@ -213,28 +132,6 @@ version = "3.4.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" },
{ url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" },
{ url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" },
{ url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" },
{ url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" },
{ url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" },
{ url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" },
{ url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" },
{ url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" },
{ url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
{ url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
{ url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
{ url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
{ url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
{ url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
{ url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
{ url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
{ url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
@@ -298,31 +195,6 @@ version = "7.13.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" },
{ url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" },
{ url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" },
{ url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" },
{ url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" },
{ url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" },
{ url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" },
{ url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" },
{ url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" },
{ url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" },
{ url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" },
{ url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" },
{ url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" },
{ url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" },
{ url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" },
{ url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" },
{ url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" },
{ url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" },
{ url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" },
{ url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" },
{ url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" },
{ url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" },
{ url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" },
{ url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" },
{ url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" },
{ url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" },
{ url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" },
@@ -391,18 +263,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" },
] ]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.1" version = "46.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" }
wheels = [ wheels = [
@@ -451,14 +317,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" },
{ url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" },
{ url = "https://files.pythonhosted.org/packages/14/b9/b260180b31a66859648cfed5c980544ee22b15f8bd20ef82a23f58c0b83e/cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d", size = 3714683, upload-time = "2025-09-17T00:10:15.601Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5a/1cd3ef86e5884edcbf8b27c3aa8f9544e9b9fcce5d3ed8b86959741f4f8e/cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5", size = 3443784, upload-time = "2025-09-17T00:10:18.014Z" },
{ url = "https://files.pythonhosted.org/packages/27/27/077e09fd92075dd1338ea0ffaf5cfee641535545925768350ad90d8c36ca/cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70", size = 3722319, upload-time = "2025-09-17T00:10:20.273Z" },
{ url = "https://files.pythonhosted.org/packages/db/32/6fc7250280920418651640d76cee34d91c1e0601d73acd44364570cf041f/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f", size = 4249030, upload-time = "2025-09-17T00:10:22.396Z" },
{ url = "https://files.pythonhosted.org/packages/32/33/8d5398b2da15a15110b2478480ab512609f95b45ead3a105c9a9c76f9980/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc", size = 4528009, upload-time = "2025-09-17T00:10:24.418Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1c/4012edad2a8977ab386c36b6e21f5065974d37afa3eade83a9968cba4855/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d", size = 4248902, upload-time = "2025-09-17T00:10:26.255Z" },
{ url = "https://files.pythonhosted.org/packages/58/a3/257cd5ae677302de8fa066fca9de37128f6729d1e63c04dd6a15555dd450/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46", size = 4527150, upload-time = "2025-09-17T00:10:28.28Z" },
{ url = "https://files.pythonhosted.org/packages/6a/cd/fe6b65e1117ec7631f6be8951d3db076bac3e1b096e3e12710ed071ffc3c/cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a", size = 3448210, upload-time = "2025-09-17T00:10:30.145Z" },
] ]
[[package]] [[package]]
@@ -470,7 +328,6 @@ dependencies = [
{ name = "docstring-parser", marker = "python_full_version < '4'" }, { name = "docstring-parser", marker = "python_full_version < '4'" },
{ name = "rich" }, { name = "rich" },
{ name = "rich-rst" }, { name = "rich-rst" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" } sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" }
wheels = [ wheels = [
@@ -593,20 +450,6 @@ version = "0.7.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" },
{ url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" },
{ url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" },
{ url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" },
{ url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" },
{ url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" },
{ url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" },
{ url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" },
{ url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" },
{ url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" },
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
@@ -675,18 +518,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
] ]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@@ -721,9 +552,6 @@ wheels = [
name = "jaraco-context" name = "jaraco-context"
version = "6.1.0" version = "6.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" },
@@ -797,7 +625,6 @@ name = "keyring"
version = "25.7.0" version = "25.7.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
{ name = "jaraco-classes" }, { name = "jaraco-classes" },
{ name = "jaraco-context" }, { name = "jaraco-context" },
{ name = "jaraco-functools" }, { name = "jaraco-functools" },
@@ -816,18 +643,6 @@ version = "1.12.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" },
{ url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" },
{ url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" },
{ url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" },
{ url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" },
{ url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" },
{ url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" },
{ url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" },
{ url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" },
{ url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" },
{ url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" },
{ url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" },
@@ -852,7 +667,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" },
{ url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" },
{ url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" },
] ]
[[package]] [[package]]
@@ -873,28 +687,6 @@ version = "3.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
@@ -992,15 +784,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
] ]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]] [[package]]
name = "nh3" name = "nh3"
version = "0.3.2" version = "0.3.2"
@@ -1122,24 +905,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
] ]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -1187,33 +952,6 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
{ url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
{ url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
{ url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
{ url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
{ url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
{ url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
{ url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
@@ -1245,24 +983,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
{ url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
{ url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
{ url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
{ url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
] ]
[[package]] [[package]]
@@ -1312,12 +1032,10 @@ version = "8.4.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" }, { name = "iniconfig" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [ wheels = [
@@ -1329,7 +1047,6 @@ name = "pytest-asyncio"
version = "1.2.0" version = "1.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" }, { name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" },
] ]
@@ -1343,7 +1060,7 @@ name = "pytest-cov"
version = "7.0.0" version = "7.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "coverage", extra = ["toml"] }, { name = "coverage" },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pytest" }, { name = "pytest" },
] ]
@@ -1370,15 +1087,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
] ]
[[package]]
name = "pytokens"
version = "0.1.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" },
]
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2025.2" version = "2025.2"
@@ -1393,12 +1101,6 @@ name = "pywin32"
version = "311" version = "311"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
{ url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
{ url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
@@ -1425,24 +1127,6 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
@@ -1591,35 +1275,6 @@ version = "0.27.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" },
{ url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" },
{ url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" },
{ url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" },
{ url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" },
{ url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" },
{ url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" },
{ url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" },
{ url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" },
{ url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" },
{ url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" },
{ url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" },
{ url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" },
{ url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" },
{ url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" },
{ url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" },
{ url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" },
{ url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" },
{ url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" },
{ url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" },
{ url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" },
{ url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" },
{ url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" },
{ url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" },
{ url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" },
@@ -1693,31 +1348,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" },
{ url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" },
{ url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" },
{ url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" },
{ url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" },
{ url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" },
{ url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" },
{ url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" },
{ url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" },
{ url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" },
{ url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" },
{ url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" },
{ url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" },
{ url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" },
{ url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" },
{ url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" },
{ url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" },
{ url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" },
{ url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" },
{ url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" },
{ url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" },
{ url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" },
{ url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" },
] ]
[[package]] [[package]]
@@ -1802,45 +1432,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
] ]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]] [[package]]
name = "twine" name = "twine"
version = "6.2.0" version = "6.2.0"
@@ -1930,46 +1521,8 @@ dependencies = [
{ name = "websockets" }, { name = "websockets" },
] ]
[package.optional-dependencies]
all = [
{ name = "black" },
{ name = "build" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
{ name = "twine" },
{ name = "ty" },
{ name = "types-pytz" },
]
dev = [
{ name = "black" },
{ name = "build" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
{ name = "twine" },
{ name = "ty" },
{ name = "types-pytz" },
]
lint = [
{ name = "black" },
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
]
types = [
{ name = "types-pytz" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "black" },
{ name = "build" }, { name = "build" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
@@ -1982,37 +1535,18 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "black", marker = "extra == 'dev'", specifier = ">=25.1.0" },
{ name = "black", marker = "extra == 'lint'", specifier = ">=25.1.0" },
{ name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2" },
{ name = "fastapi", specifier = ">=0.115.0" }, { name = "fastapi", specifier = ">=0.115.0" },
{ name = "fastmcp", specifier = ">=2.11.2" }, { name = "fastmcp", specifier = ">=2.11.2" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.2.0" },
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.2.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=7.0.0" },
{ name = "python-dotenv", specifier = ">=1.1.1" }, { name = "python-dotenv", specifier = ">=1.1.1" },
{ name = "pytz", specifier = ">=2025.2" }, { name = "pytz", specifier = ">=2025.2" },
{ name = "rich", specifier = ">=14.1.0" }, { name = "rich", specifier = ">=14.1.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.8" },
{ name = "ruff", marker = "extra == 'lint'", specifier = ">=0.12.8" },
{ name = "twine", marker = "extra == 'dev'", specifier = ">=6.0.1" },
{ name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.15" },
{ name = "ty", marker = "extra == 'lint'", specifier = ">=0.0.15" },
{ name = "types-pytz", marker = "extra == 'dev'", specifier = ">=2025.2.0.20250809" },
{ name = "types-pytz", marker = "extra == 'types'", specifier = ">=2025.2.0.20250809" },
{ name = "unraid-mcp", extras = ["dev", "test", "lint", "types"], marker = "extra == 'all'" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
{ name = "websockets", specifier = ">=13.1,<14.0" }, { name = "websockets", specifier = ">=13.1,<14.0" },
] ]
provides-extras = ["dev", "test", "lint", "types", "all"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "black", specifier = ">=25.1.0" },
{ name = "build", specifier = ">=1.2.2" }, { name = "build", specifier = ">=1.2.2" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-asyncio", specifier = ">=1.2.0" },
@@ -2039,7 +1573,6 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
wheels = [ wheels = [
@@ -2063,18 +1596,6 @@ version = "0.22.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" },
{ url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" },
{ url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" },
{ url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" },
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
@@ -2110,31 +1631,6 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" },
{ url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" },
{ url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" },
{ url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" },
{ url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" },
{ url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" },
{ url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" },
{ url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" },
{ url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" },
{ url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
@@ -2194,14 +1690,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
{ url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" },
{ url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" },
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
] ]
[[package]] [[package]]
@@ -2210,28 +1698,6 @@ version = "13.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" },
{ url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" },
{ url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" },
{ url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" },
{ url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" },
{ url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" },
{ url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" },
{ url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" },
{ url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" },
{ url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379, upload-time = "2024-09-21T17:32:45.933Z" },
{ url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376, upload-time = "2024-09-21T17:32:46.987Z" },
{ url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753, upload-time = "2024-09-21T17:32:48.046Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051, upload-time = "2024-09-21T17:32:49.271Z" },
{ url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489, upload-time = "2024-09-21T17:32:50.392Z" },
{ url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438, upload-time = "2024-09-21T17:32:52.223Z" },
{ url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710, upload-time = "2024-09-21T17:32:53.244Z" },
{ url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137, upload-time = "2024-09-21T17:32:54.721Z" },
{ url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" },
{ url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" },
{ url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" },
@@ -2254,12 +1720,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" },
{ url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" },
{ url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" },
{ url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" },
{ url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" },
{ url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" },
{ url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" },
{ url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" },
] ]
@@ -2274,12 +1734,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" },
] ]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]