forked from HomeLab/unraid-mcp
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:
@@ -27,10 +27,11 @@ This directory contains the Claude Code marketplace configuration for the Unraid
|
||||
## Available Plugins
|
||||
|
||||
### unraid
|
||||
|
||||
Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring.
|
||||
|
||||
**Features:**
|
||||
- 27 read-only API endpoints
|
||||
- 10 tools with 90 actions (queries and mutations)
|
||||
- Real-time system metrics
|
||||
- Disk health and temperature monitoring
|
||||
- Docker container management
|
||||
@@ -39,7 +40,7 @@ Query and monitor Unraid servers via GraphQL API - array status, disk health, co
|
||||
- Network share information
|
||||
- Notification management
|
||||
|
||||
**Version:** 1.1.0
|
||||
**Version:** 0.2.0
|
||||
**Category:** Infrastructure
|
||||
**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:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unraid-mcp",
|
||||
"description": "Comprehensive Unraid server management and monitoring tools via GraphQL API",
|
||||
"version": "1.0.0",
|
||||
"version": "0.2.0",
|
||||
"owner": {
|
||||
"name": "jmagar",
|
||||
"email": "jmagar@users.noreply.github.com",
|
||||
@@ -12,9 +12,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "unraid",
|
||||
"source": "./skills/unraid",
|
||||
"source": "./",
|
||||
"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"],
|
||||
"category": "infrastructure"
|
||||
}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -34,11 +34,11 @@ logs/
|
||||
# Documentation and session artifacts
|
||||
.docs/
|
||||
.full-review/
|
||||
docs/plans/
|
||||
docs/sessions/
|
||||
/docs/plans/
|
||||
/docs/sessions/
|
||||
|
||||
# Test planning documents
|
||||
DESTRUCTIVE_ACTIONS.md
|
||||
/DESTRUCTIVE_ACTIONS.md
|
||||
|
||||
# Google OAuth client secrets
|
||||
client_secret_*.apps.googleusercontent.com.json
|
||||
|
||||
19
CLAUDE.md
19
CLAUDE.md
@@ -21,23 +21,18 @@ uv sync --group dev
|
||||
# Local development with uv (recommended)
|
||||
uv run unraid-mcp-server
|
||||
|
||||
# Using development script with hot reload
|
||||
./dev.sh
|
||||
|
||||
# Direct module execution
|
||||
uv run -m unraid_mcp.main
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Format code with black
|
||||
uv run black unraid_mcp/
|
||||
|
||||
# Lint with ruff
|
||||
# Lint and format with ruff
|
||||
uv run ruff check unraid_mcp/
|
||||
uv run ruff format unraid_mcp/
|
||||
|
||||
# Type checking with mypy
|
||||
uv run mypy unraid_mcp/
|
||||
# Type checking with ty (Astral's fast type checker)
|
||||
uv run ty check unraid_mcp/
|
||||
|
||||
# Run tests
|
||||
uv run pytest
|
||||
@@ -49,13 +44,13 @@ uv run pytest
|
||||
docker build -t unraid-mcp-server .
|
||||
|
||||
# Run with Docker Compose
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f unraid-mcp
|
||||
docker compose logs -f unraid-mcp
|
||||
|
||||
# Stop service
|
||||
docker-compose down
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
@@ -130,8 +130,8 @@ Before publishing to GitHub:
|
||||
|
||||
5. **Create Release Tag** (Optional)
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
git push origin v1.0.0
|
||||
git tag -a v0.2.0 -m "Release v0.2.0"
|
||||
git push origin v0.2.0
|
||||
```
|
||||
|
||||
## User Experience
|
||||
|
||||
@@ -60,11 +60,9 @@ rm -rf dist/ build/ *.egg-info/
|
||||
### 2. Run Quality Checks
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
uv run black unraid_mcp/
|
||||
|
||||
# Lint code
|
||||
# Lint and format code
|
||||
uv run ruff check unraid_mcp/
|
||||
uv run ruff format unraid_mcp/
|
||||
|
||||
# Type check
|
||||
uv run ty check unraid_mcp/
|
||||
@@ -187,6 +185,8 @@ jobs:
|
||||
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
|
||||
|
||||
### "File already exists" Error
|
||||
|
||||
99
README.md
99
README.md
@@ -1,6 +1,6 @@
|
||||
# 🚀 Unraid MCP Server
|
||||
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://github.com/jlowin/fastmcp)
|
||||
[](LICENSE)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## ✨ 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
|
||||
- ⚡ **High Performance**: Async/concurrent operations with optimized timeouts
|
||||
- 🔄 **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:
|
||||
- 27 GraphQL API endpoints
|
||||
- 10 tools exposing 90 actions via the consolidated action pattern
|
||||
- Real-time system metrics
|
||||
- Disk health monitoring
|
||||
- Docker and VM management
|
||||
@@ -58,7 +58,7 @@ This provides instant access to Unraid monitoring and management through Claude
|
||||
|
||||
### Prerequisites
|
||||
- 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
|
||||
|
||||
### 1. Clone Repository
|
||||
@@ -88,7 +88,7 @@ docker compose logs -f unraid-mcp
|
||||
uv sync
|
||||
|
||||
# Run development server
|
||||
./dev.sh
|
||||
uv run unraid-mcp-server
|
||||
```
|
||||
|
||||
---
|
||||
@@ -147,7 +147,7 @@ cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
# 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
|
||||
|
||||
### System Information & Status
|
||||
- `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
|
||||
Each tool uses a consolidated `action` parameter to expose multiple operations, reducing context window usage. Destructive actions require `confirm=True`.
|
||||
|
||||
### Docker Container Management
|
||||
- `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
|
||||
### Tool Categories (10 Tools, 90 Actions)
|
||||
|
||||
### Virtual Machine Management
|
||||
- `list_vms()` - List all VMs and their states
|
||||
- `manage_vm(id, action)` - VM lifecycle (start/stop/pause/resume/reboot)
|
||||
- `get_vm_details(identifier)` - Detailed VM information
|
||||
|
||||
### Storage & File Systems
|
||||
- `get_shares_info()` - User shares information
|
||||
- `list_physical_disks()` - Physical disk discovery
|
||||
- `get_disk_details(disk_id)` - SMART data and detailed disk info
|
||||
|
||||
### Monitoring & Diagnostics
|
||||
- `health_check()` - Comprehensive system health assessment
|
||||
- `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
|
||||
| Tool | Actions | Description |
|
||||
|------|---------|-------------|
|
||||
| **`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 |
|
||||
| **`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 |
|
||||
| **`unraid_docker`** | 15 | list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates |
|
||||
| **`unraid_vm`** | 9 | list, details, start, stop, pause, resume, force_stop, reboot, reset |
|
||||
| **`unraid_notifications`** | 9 | overview, list, warnings, create, archive, unread, delete, delete_archived, archive_all |
|
||||
| **`unraid_rclone`** | 4 | list_remotes, config_form, create_remote, delete_remote |
|
||||
| **`unraid_users`** | 8 | me, list, get, add, delete, cloud, remote_access, origins |
|
||||
| **`unraid_keys`** | 5 | list, get, create, update, delete |
|
||||
| **`unraid_health`** | 3 | check, test_connection, diagnose |
|
||||
|
||||
### MCP Resources (Real-time Data)
|
||||
- `unraid://logs/stream` - Live log streaming from `/var/log/syslog` with WebSocket subscriptions
|
||||
@@ -257,26 +234,27 @@ unraid-mcp/
|
||||
│ │ ├── manager.py # WebSocket management
|
||||
│ │ ├── resources.py # MCP resources
|
||||
│ │ └── diagnostics.py # Diagnostic tools
|
||||
│ ├── tools/ # MCP tool categories
|
||||
│ │ ├── docker.py # Container management
|
||||
│ │ ├── system.py # System information
|
||||
│ │ ├── storage.py # Storage & monitoring
|
||||
│ │ ├── health.py # Health checks
|
||||
│ │ ├── virtualization.py # VM management
|
||||
│ │ └── rclone.py # Cloud storage
|
||||
│ ├── tools/ # MCP tool categories (10 tools, 90 actions)
|
||||
│ │ ├── info.py # System information (19 actions)
|
||||
│ │ ├── array.py # Array management (12 actions)
|
||||
│ │ ├── storage.py # Storage & monitoring (6 actions)
|
||||
│ │ ├── docker.py # Container management (15 actions)
|
||||
│ │ ├── virtualization.py # VM management (9 actions)
|
||||
│ │ ├── 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
|
||||
├── logs/ # Log files (auto-created)
|
||||
├── dev.sh # Development script
|
||||
└── docker-compose.yml # Docker Compose deployment
|
||||
```
|
||||
|
||||
### Code Quality Commands
|
||||
```bash
|
||||
# Format code
|
||||
uv run black unraid_mcp/
|
||||
|
||||
# Lint code
|
||||
# Lint and format code
|
||||
uv run ruff check unraid_mcp/
|
||||
uv run ruff format unraid_mcp/
|
||||
|
||||
# Type checking
|
||||
uv run ty check unraid_mcp/
|
||||
@@ -287,11 +265,11 @@ uv run pytest
|
||||
|
||||
### Development Workflow
|
||||
```bash
|
||||
# Start development server (kills existing processes safely)
|
||||
./dev.sh
|
||||
# Start development server
|
||||
uv run unraid-mcp-server
|
||||
|
||||
# Stop server only
|
||||
./dev.sh --kill
|
||||
# Or run via module directly
|
||||
uv run -m unraid_mcp.main
|
||||
```
|
||||
|
||||
---
|
||||
@@ -324,7 +302,8 @@ uv run pytest
|
||||
|
||||
**🔥 Port Already in Use**
|
||||
```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**
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Competitive Analysis: Unraid Integration Projects
|
||||
|
||||
> **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
|
||||
|
||||
- [Executive Summary](#executive-summary)
|
||||
- [Project Profiles](#project-profiles)
|
||||
- [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)
|
||||
- [4. PSUnraid (PowerShell module)](#4-psunraid)
|
||||
- [5. ha-unraid (Home Assistant integration)](#5-ha-unraid)
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
## 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):**
|
||||
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:**
|
||||
- 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
|
||||
- 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
|
||||
- Real-time WebSocket event streaming (9 event types, 5-60s intervals)
|
||||
- 41 Prometheus metrics for Grafana dashboards
|
||||
@@ -341,7 +341,7 @@ The project's documentation explicitly compares SSH vs API capabilities:
|
||||
|
||||
### 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 |
|
||||
| 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 |
|
||||
|---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| 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 Prompts | N | Y (3) | N | N | N | N | N |
|
||||
| REST API | N | Y (59) | N | N | N | N | N |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Unraid API Feature Gap Analysis
|
||||
|
||||
> **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.
|
||||
|
||||
---
|
||||
@@ -497,7 +497,7 @@ GRAPHQL_PUBSUB_CHANNEL {
|
||||
|
||||
## 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
|
||||
|
||||
@@ -663,7 +663,7 @@ The current MCP server has 26 tools. The following capabilities are available in
|
||||
| **Queries** | ~30+ | 14 | ~16+ uncovered |
|
||||
| **Mutations** | ~50+ | 10 (start/stop Docker+VM, RClone CRUD) | ~40+ 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.**
|
||||
|
||||
@@ -691,7 +691,7 @@ Capabilities this project offers that we do NOT:
|
||||
| **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) |
|
||||
| **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 |
|
||||
|
||||
### G.2 PSUnraid (PowerShell Module)
|
||||
@@ -828,7 +828,7 @@ Chose SSH over GraphQL API due to these gaps:
|
||||
| | **Low Priority Subtotal** | **18** | **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**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -883,76 +884,3 @@ With the [Unraid Patch plugin](https://forums.unraid.net/topic/185560-unraid-pat
|
||||
* 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.
|
||||
|
||||
* [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)
|
||||
@@ -468,6 +468,9 @@ unraid-api apikey --delete --name "workflow key" --json
|
||||
#!/bin/bash
|
||||
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
|
||||
echo "Creating temporary API key..."
|
||||
KEY_DATA=$(unraid-api apikey --create \
|
||||
@@ -476,20 +479,20 @@ KEY_DATA=$(unraid-api apikey --create \
|
||||
--description "Temporary key for deployment $(date)" \
|
||||
--json)
|
||||
|
||||
# Register trap after key creation succeeds
|
||||
trap cleanup EXIT
|
||||
|
||||
# 2. Extract the API key
|
||||
API_KEY=$(echo "$KEY_DATA" | jq -r '.key')
|
||||
echo "API key created successfully"
|
||||
|
||||
# 3. Use the key for operations
|
||||
echo "Configuring services..."
|
||||
curl -H "Authorization: Bearer $API_KEY" \
|
||||
curl -H "x-api-key: $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"provider": "azure", "clientId": "your-client-id"}' \
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```graphql
|
||||
|
||||
@@ -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/)):
|
||||
|
||||
```
|
||||
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
|
||||
@@ -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)):
|
||||
|
||||
- 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
|
||||
- Listed on Glama, MCP Market, MCPServers.com, LangDB, UBOS, JuheAPI
|
||||
- 21 GitHub stars
|
||||
|
||||
@@ -322,6 +322,7 @@ query {
|
||||
```
|
||||
|
||||
#### RClone Remotes (`rclone`)
|
||||
|
||||
```graphql
|
||||
query {
|
||||
rclone {
|
||||
@@ -334,6 +335,7 @@ query {
|
||||
```
|
||||
|
||||
#### Notifications
|
||||
|
||||
```graphql
|
||||
query {
|
||||
notifications {
|
||||
@@ -343,6 +345,7 @@ query {
|
||||
```
|
||||
|
||||
#### Shares
|
||||
|
||||
```graphql
|
||||
query {
|
||||
shares {
|
||||
@@ -354,6 +357,7 @@ query {
|
||||
### Mutation Types
|
||||
|
||||
#### Docker Container Management
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
docker {
|
||||
@@ -366,6 +370,7 @@ mutation {
|
||||
- Mutations are idempotent (starting an already-running container returns success)
|
||||
|
||||
#### VM Management
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
vm {
|
||||
|
||||
@@ -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"
|
||||
readme = "README.md"
|
||||
license = {file = "LICENSE"}
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.12"
|
||||
authors = [
|
||||
{name = "jmagar", email = "jmagar@users.noreply.github.com"}
|
||||
]
|
||||
@@ -46,8 +46,6 @@ classifiers = [
|
||||
|
||||
# Python Versions
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
@@ -82,44 +80,6 @@ dependencies = [
|
||||
"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
|
||||
# ============================================================================
|
||||
@@ -142,7 +102,6 @@ unraid-mcp = "unraid_mcp.main:main"
|
||||
# Build Configuration
|
||||
# ============================================================================
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["unraid_mcp"]
|
||||
only-include = ["unraid_mcp"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
@@ -167,33 +126,10 @@ exclude = [
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Tool Configuration: Black (Code 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 Configuration: Ruff (Linting & Formatting)
|
||||
# ============================================================================
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
target-version = "py312"
|
||||
line-length = 100
|
||||
cache-dir = ".cache/.ruff_cache"
|
||||
|
||||
@@ -236,7 +172,7 @@ select = [
|
||||
"RUF",
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by black)
|
||||
"E501", # line too long (handled by ruff formatter)
|
||||
"B008", # function calls in argument defaults
|
||||
"C901", # too complex
|
||||
"D100", # missing docstring in public module
|
||||
@@ -266,7 +202,7 @@ lines-after-imports = 2
|
||||
# Tool Configuration: ty (Type Checking)
|
||||
# ============================================================================
|
||||
[tool.ty.environment]
|
||||
python-version = "3.10"
|
||||
python-version = "3.12"
|
||||
|
||||
[tool.ty.analysis]
|
||||
respect-type-ignore-comments = true
|
||||
@@ -351,7 +287,6 @@ dev = [
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ set -euo pipefail
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 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"
|
||||
|
||||
# Verify source path
|
||||
PLUGIN_SOURCE=$(jq -r '.plugins[] | select(.name == "unraid") | .source' .claude-plugin/marketplace.json)
|
||||
check "Plugin source path is valid" "test -d \"$PLUGIN_SOURCE\""
|
||||
PLUGIN_SOURCE=$(jq -r '.plugins[]? | select(.name == "unraid") | .source // empty' .claude-plugin/marketplace.json 2>/dev/null || true)
|
||||
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 "=== Results ==="
|
||||
@@ -76,5 +81,5 @@ else
|
||||
echo -e "${GREEN}All checks passed!${NC}"
|
||||
echo ""
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unraid",
|
||||
"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": {
|
||||
"name": "jmagar",
|
||||
"email": "jmagar@users.noreply.github.com"
|
||||
@@ -14,7 +14,7 @@
|
||||
"args": [
|
||||
"run",
|
||||
"--directory",
|
||||
"${CLAUDE_PLUGIN_ROOT}/../..",
|
||||
"${CLAUDE_PLUGIN_ROOT}",
|
||||
"unraid-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
|
||||
@@ -143,7 +143,7 @@ This skill activates when you mention:
|
||||
|
||||
## Version
|
||||
|
||||
- **Skill Version:** 1.0.0
|
||||
- **Skill Version:** 0.2.0
|
||||
- **API Version:** Unraid 7.2 GraphQL
|
||||
- **Tested:** 2026-01-21
|
||||
- **Endpoints:** 27 working read-only queries
|
||||
|
||||
@@ -123,7 +123,7 @@ Always use `metrics` for monitoring, `info` for specifications.
|
||||
|
||||
**List VMs:**
|
||||
```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.
|
||||
|
||||
@@ -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.)
|
||||
3. **Always check errors** - GraphQL returns errors in `errors` array
|
||||
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
|
||||
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
|
||||
@@ -926,8 +926,8 @@ Common differences from online documentation:
|
||||
|
||||
## 🚫 Known Limitations
|
||||
|
||||
1. **No Docker container logs** - Container output logs are NOT accessible via API
|
||||
2. **No real-time streaming** - All queries are request/response, no WebSocket subscriptions
|
||||
1. **No Docker container logs** - Container output logs are NOT accessible via the read-only query API (use `docker.logs` mutation)
|
||||
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
|
||||
4. **No mutation examples included** - This guide covers read-only queries only
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Complete list of available GraphQL read-only endpoints in Unraid 7.2+.
|
||||
19. **`isSSOEnabled`** - SSO status
|
||||
20. **`oidcProviders`** - OIDC providers
|
||||
|
||||
## API Management (2)
|
||||
## API Management (1)
|
||||
21. **`apiKeys`** - List API keys
|
||||
|
||||
## Customization (3)
|
||||
|
||||
@@ -10,7 +10,7 @@ export UNRAID_URL="https://your-unraid-server/graphql"
|
||||
export UNRAID_API_KEY="your-api-key-here"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
Common issues and solutions when working with the Unraid GraphQL API.
|
||||
|
||||
## "Cannot query field" error
|
||||
|
||||
Field name doesn't exist in your Unraid version. Use introspection to find valid fields:
|
||||
|
||||
```bash
|
||||
./scripts/unraid-query.sh -q "{ __type(name: \"TypeName\") { fields { name } } }"
|
||||
```
|
||||
|
||||
@@ -13,10 +13,23 @@ OUTPUT_FILE="$HOME/memory/bank/unraid-inventory.md"
|
||||
|
||||
# Load credentials from .env for all servers
|
||||
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"
|
||||
key_var="UNRAID_${server}_API_KEY"
|
||||
name_var="UNRAID_${server}_NAME"
|
||||
validate_env_vars "$url_var" "$key_var" || exit 1
|
||||
done
|
||||
|
||||
@@ -36,9 +49,10 @@ process_server() {
|
||||
|
||||
echo "Querying server: $NAME..."
|
||||
|
||||
export UNRAID_URL="$URL"
|
||||
export UNRAID_API_KEY="$API_KEY"
|
||||
export IGNORE_ERRORS="true"
|
||||
UNRAID_URL="$URL"
|
||||
UNRAID_API_KEY="$API_KEY"
|
||||
IGNORE_ERRORS="true"
|
||||
export UNRAID_URL UNRAID_API_KEY IGNORE_ERRORS
|
||||
|
||||
QUERY='query Dashboard {
|
||||
info {
|
||||
@@ -73,13 +87,16 @@ process_server() {
|
||||
|
||||
RESPONSE=$("$QUERY_SCRIPT" -q "$QUERY" -f json)
|
||||
|
||||
# Debug output
|
||||
echo "$RESPONSE" > "${NAME}_debug.json"
|
||||
# Debug output (only when DEBUG is set)
|
||||
if [ "${DEBUG:-}" = "true" ]; then
|
||||
echo "$RESPONSE" > "/tmp/${NAME}_debug.json"
|
||||
fi
|
||||
|
||||
# Check if response is valid JSON
|
||||
if ! echo "$RESPONSE" | jq -e . >/dev/null 2>&1; then
|
||||
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 "Failed to retrieve data." >> "$OUTPUT_FILE"
|
||||
return
|
||||
@@ -115,32 +132,30 @@ process_server() {
|
||||
|
||||
# Array capacity
|
||||
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')
|
||||
|
||||
if [ "$ARRAY_TOTAL" != "null" ] && [ "$ARRAY_TOTAL" -gt 0 ]; then
|
||||
ARRAY_TOTAL_GB=$((ARRAY_TOTAL / 1024 / 1024))
|
||||
ARRAY_FREE_GB=$((ARRAY_FREE / 1024 / 1024))
|
||||
ARRAY_USED_GB=$((ARRAY_USED / 1024 / 1024))
|
||||
ARRAY_USED_PCT=$((ARRAY_USED * 100 / ARRAY_TOTAL))
|
||||
echo "### Storage" >> "$OUTPUT_FILE"
|
||||
echo "- **Array:** ${ARRAY_USED_GB}GB / ${ARRAY_TOTAL_GB}GB used (${ARRAY_USED_PCT}%)" >> "$OUTPUT_FILE"
|
||||
|
||||
# Cache pools
|
||||
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"
|
||||
fi
|
||||
|
||||
# Cache pools
|
||||
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"
|
||||
|
||||
# Docker
|
||||
TOTAL_CONTAINERS=$(echo "$RESPONSE" | jq '[.data.docker.containers[]] | length')
|
||||
RUNNING_CONTAINERS=$(echo "$RESPONSE" | jq '[.data.docker.containers[] | select(.state == "RUNNING")] | length')
|
||||
TOTAL_CONTAINERS=$(echo "$RESPONSE" | jq '[(.data.docker.containers // [])[]] | length')
|
||||
RUNNING_CONTAINERS=$(echo "$RESPONSE" | jq '[(.data.docker.containers // [])[] | select(.state == "RUNNING")] | length')
|
||||
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
echo "### Workloads" >> "$OUTPUT_FILE"
|
||||
echo "- **Docker:** ${TOTAL_CONTAINERS} containers (${RUNNING_CONTAINERS} running)" >> "$OUTPUT_FILE"
|
||||
|
||||
# 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
|
||||
echo "$UNHEALTHY" >> "$OUTPUT_FILE"
|
||||
fi
|
||||
@@ -173,7 +188,7 @@ process_server() {
|
||||
echo "### Notifications" >> "$OUTPUT_FILE"
|
||||
|
||||
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)
|
||||
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
|
||||
@@ -198,7 +213,7 @@ process_server() {
|
||||
}
|
||||
|
||||
# Main loop - process each server from environment variables
|
||||
for server in "TOOTIE" "SHART"; do
|
||||
for server in "${SERVERS[@]}"; do
|
||||
name_var="UNRAID_${server}_NAME"
|
||||
url_var="UNRAID_${server}_URL"
|
||||
key_var="UNRAID_${server}_API_KEY"
|
||||
|
||||
@@ -16,6 +16,8 @@ OPTIONS:
|
||||
-k, --key KEY API key (required)
|
||||
-q, --query QUERY GraphQL query (required)
|
||||
-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
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
@@ -43,6 +45,8 @@ URL="${UNRAID_URL:-}"
|
||||
API_KEY="${UNRAID_API_KEY:-}"
|
||||
QUERY=""
|
||||
FORMAT="json"
|
||||
IGNORE_ERRORS="${IGNORE_ERRORS:-false}"
|
||||
INSECURE="false"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -63,6 +67,14 @@ while [[ $# -gt 0 ]]; do
|
||||
FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ignore-errors)
|
||||
IGNORE_ERRORS="true"
|
||||
shift
|
||||
;;
|
||||
--insecure)
|
||||
INSECURE="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
@@ -89,15 +101,22 @@ if [[ -z "$QUERY" ]]; then
|
||||
exit 1
|
||||
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
|
||||
RESPONSE=$(curl -skL -X POST "$URL" \
|
||||
RESPONSE=$(curl "${CURL_FLAGS[@]}" "$URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: $API_KEY" \
|
||||
-d "{\"query\":\"$QUERY\"}")
|
||||
-d "$PAYLOAD")
|
||||
|
||||
# Check for errors
|
||||
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
|
||||
echo "GraphQL Warning:" >&2
|
||||
echo "$RESPONSE" | jq -r '.errors[0].message' >&2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Shared test fixtures and helpers for Unraid MCP server tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -8,7 +9,7 @@ from fastmcp import FastMCP
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
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)
|
||||
test_mcp = FastMCP("test")
|
||||
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]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_array tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -49,10 +50,10 @@ class TestArrayActions:
|
||||
call_args = _mock_graphql.call_args
|
||||
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}}}
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="parity_history")
|
||||
result = await tool_fn(action="parity_status")
|
||||
assert result["success"] is True
|
||||
|
||||
async def test_mount_disk(self, _mock_graphql: AsyncMock) -> None:
|
||||
@@ -100,4 +101,70 @@ class TestArrayActions:
|
||||
_mock_graphql.side_effect = RuntimeError("disk error")
|
||||
tool_fn = _make_tool()
|
||||
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
474
tests/test_client.py
Normal 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 }")
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_docker tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -8,6 +9,7 @@ from conftest import make_tool_fn
|
||||
from unraid_mcp.core.exceptions import ToolError
|
||||
from unraid_mcp.tools.docker import find_container_by_identifier, get_available_container_names
|
||||
|
||||
|
||||
# --- Unit tests for helpers ---
|
||||
|
||||
|
||||
@@ -52,7 +54,7 @@ class TestGetAvailableContainerNames:
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -204,3 +206,92 @@ class TestDockerActions:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="unexpected failure"):
|
||||
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")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_health tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -104,9 +105,8 @@ class TestHealthActions:
|
||||
with patch(
|
||||
"unraid_mcp.tools.health._diagnose_subscriptions",
|
||||
side_effect=RuntimeError("broken"),
|
||||
):
|
||||
with pytest.raises(ToolError, match="broken"):
|
||||
await tool_fn(action="diagnose")
|
||||
), pytest.raises(ToolError, match="broken"):
|
||||
await tool_fn(action="diagnose")
|
||||
|
||||
async def test_diagnose_success(self, _mock_graphql: AsyncMock) -> None:
|
||||
"""Diagnose returns subscription status when modules are available."""
|
||||
@@ -120,17 +120,24 @@ class TestHealthActions:
|
||||
|
||||
async def test_diagnose_import_error_internal(self) -> None:
|
||||
"""_diagnose_subscriptions catches ImportError and returns error dict."""
|
||||
import builtins
|
||||
import sys
|
||||
|
||||
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):
|
||||
if "subscriptions" in name:
|
||||
raise ImportError("no module")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=fail_subscriptions):
|
||||
result = await _diagnose_subscriptions()
|
||||
assert "error" in result
|
||||
try:
|
||||
# Replace the modules with objects that raise ImportError on access
|
||||
with patch.dict(sys.modules, {
|
||||
"unraid_mcp.subscriptions": None,
|
||||
"unraid_mcp.subscriptions.manager": None,
|
||||
"unraid_mcp.subscriptions.resources": None,
|
||||
}):
|
||||
result = await _diagnose_subscriptions()
|
||||
assert "error" in result
|
||||
finally:
|
||||
# Restore cached modules
|
||||
sys.modules.update(cached)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_info tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -12,6 +13,7 @@ from unraid_mcp.tools.info import (
|
||||
_process_system_info,
|
||||
)
|
||||
|
||||
|
||||
# --- Unit tests for helper functions ---
|
||||
|
||||
|
||||
@@ -115,7 +117,7 @@ class TestProcessArrayStatus:
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -207,3 +209,32 @@ class TestUnraidInfoTool:
|
||||
result = await tool_fn(action="ups_devices")
|
||||
assert len(result["ups_devices"]) == 1
|
||||
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")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_keys tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -88,3 +89,9 @@ class TestKeysActions:
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="delete", key_id="k:1", confirm=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")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_notifications tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_rclone tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -18,18 +19,19 @@ def _make_tool():
|
||||
return make_tool_fn("unraid_mcp.tools.rclone", "register_rclone_tool", "unraid_rclone")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("_mock_graphql")
|
||||
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()
|
||||
with pytest.raises(ToolError, match="destructive"):
|
||||
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()
|
||||
with pytest.raises(ToolError, match="requires name"):
|
||||
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()
|
||||
with pytest.raises(ToolError, match="name is required"):
|
||||
await tool_fn(action="delete_remote", confirm=True)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_storage tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -36,7 +37,7 @@ class TestFormatBytes:
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -98,7 +99,7 @@ class TestStorageActions:
|
||||
}
|
||||
tool_fn = _make_tool()
|
||||
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"]
|
||||
|
||||
async def test_disk_details_not_found(self, _mock_graphql: AsyncMock) -> None:
|
||||
@@ -124,3 +125,32 @@ class TestStorageActions:
|
||||
tool_fn = _make_tool()
|
||||
result = await tool_fn(action="logs", log_path="/var/log/syslog")
|
||||
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")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_users tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for unraid_vm tool."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,7 +10,7 @@ from unraid_mcp.core.exceptions import ToolError
|
||||
|
||||
|
||||
@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:
|
||||
yield mock
|
||||
|
||||
@@ -128,3 +129,53 @@ class TestVmActions:
|
||||
tool_fn = _make_tool()
|
||||
with pytest.raises(ToolError, match="Failed to start"):
|
||||
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")
|
||||
|
||||
@@ -4,4 +4,4 @@ A modular MCP (Model Context Protocol) server that provides tools to interact
|
||||
with an Unraid server's GraphQL API.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
@@ -5,8 +5,8 @@ that cap at 10MB and start over (no rotation) for consistent use across all modu
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytz
|
||||
from rich.align import Align
|
||||
@@ -16,6 +16,7 @@ from rich.panel import Panel
|
||||
from rich.rule import Rule
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
try:
|
||||
from fastmcp.utilities.logging import get_logger as get_fastmcp_logger
|
||||
FASTMCP_AVAILABLE = True
|
||||
@@ -24,6 +25,7 @@ except ImportError:
|
||||
|
||||
from .settings import LOG_FILE_PATH, LOG_LEVEL_STR
|
||||
|
||||
|
||||
# Global Rich console for consistent formatting
|
||||
console = Console(stderr=True, force_terminal=True)
|
||||
|
||||
@@ -31,7 +33,7 @@ console = Console(stderr=True, force_terminal=True)
|
||||
class OverwriteFileHandler(logging.FileHandler):
|
||||
"""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.
|
||||
|
||||
Args:
|
||||
@@ -47,18 +49,19 @@ class OverwriteFileHandler(logging.FileHandler):
|
||||
def emit(self, record):
|
||||
"""Emit a record, checking file size and overwriting if needed."""
|
||||
# Check file size before writing
|
||||
if self.stream and hasattr(self.stream, 'name'):
|
||||
if self.stream and hasattr(self.stream, "name"):
|
||||
try:
|
||||
if os.path.exists(self.baseFilename):
|
||||
file_size = os.path.getsize(self.baseFilename)
|
||||
base_path = Path(self.baseFilename)
|
||||
if base_path.exists():
|
||||
file_size = base_path.stat().st_size
|
||||
if file_size >= self.max_bytes:
|
||||
# Close current stream
|
||||
if self.stream:
|
||||
self.stream.close()
|
||||
|
||||
# Remove the old file and start fresh
|
||||
if os.path.exists(self.baseFilename):
|
||||
os.remove(self.baseFilename)
|
||||
if base_path.exists():
|
||||
base_path.unlink()
|
||||
|
||||
# Reopen with truncate mode
|
||||
self.stream = self._open()
|
||||
@@ -75,9 +78,10 @@ class OverwriteFileHandler(logging.FileHandler):
|
||||
)
|
||||
super().emit(reset_record)
|
||||
|
||||
except OSError:
|
||||
# If there's an issue checking file size, just continue normally
|
||||
pass
|
||||
except OSError as e:
|
||||
import sys
|
||||
print(f"WARNING: Log file size check failed: {e}. Continuing without rotation.",
|
||||
file=sys.stderr)
|
||||
|
||||
# Emit the original record
|
||||
super().emit(record)
|
||||
@@ -119,11 +123,11 @@ def setup_logger(name: str = "UnraidMCPServer") -> logging.Logger:
|
||||
file_handler = OverwriteFileHandler(
|
||||
LOG_FILE_PATH,
|
||||
max_bytes=10*1024*1024,
|
||||
encoding='utf-8'
|
||||
encoding="utf-8"
|
||||
)
|
||||
file_handler.setLevel(numeric_log_level)
|
||||
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)
|
||||
logger.addHandler(file_handler)
|
||||
@@ -163,11 +167,11 @@ def configure_fastmcp_logger_with_rich() -> logging.Logger | None:
|
||||
file_handler = OverwriteFileHandler(
|
||||
LOG_FILE_PATH,
|
||||
max_bytes=10*1024*1024,
|
||||
encoding='utf-8'
|
||||
encoding="utf-8"
|
||||
)
|
||||
file_handler.setLevel(numeric_log_level)
|
||||
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)
|
||||
fastmcp_logger.addHandler(file_handler)
|
||||
@@ -196,7 +200,7 @@ def configure_fastmcp_logger_with_rich() -> logging.Logger | None:
|
||||
root_file_handler = OverwriteFileHandler(
|
||||
LOG_FILE_PATH,
|
||||
max_bytes=10*1024*1024,
|
||||
encoding='utf-8'
|
||||
encoding="utf-8"
|
||||
)
|
||||
root_file_handler.setLevel(numeric_log_level)
|
||||
root_file_handler.setFormatter(file_formatter)
|
||||
@@ -225,12 +229,12 @@ def log_configuration_status(logger: logging.Logger) -> None:
|
||||
config = get_config_summary()
|
||||
|
||||
# Log configuration status
|
||||
if config['api_url_configured']:
|
||||
if config["api_url_configured"]:
|
||||
logger.info(f"UNRAID_API_URL loaded: {config['api_url_preview']}")
|
||||
else:
|
||||
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
|
||||
else:
|
||||
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_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']}")
|
||||
|
||||
|
||||
# Development logging helpers for Rich formatting
|
||||
def get_est_timestamp() -> str:
|
||||
"""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)
|
||||
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
|
||||
"warning": {"color": "#EBCB8B", "icon": "⚠️", "style": ""}, # Nordic yellow
|
||||
"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
|
||||
"debug": {"color": "#4C566A", "icon": "🐛", "style": ""}, # Nordic dark gray
|
||||
}
|
||||
@@ -328,11 +332,7 @@ def log_status(message: str, indent: int = 0) -> None:
|
||||
if FASTMCP_AVAILABLE:
|
||||
# Use FastMCP logger with Rich formatting
|
||||
_fastmcp_logger = configure_fastmcp_logger_with_rich()
|
||||
if _fastmcp_logger is not None:
|
||||
logger = _fastmcp_logger
|
||||
else:
|
||||
# Fallback to our custom logger if FastMCP configuration fails
|
||||
logger = setup_logger()
|
||||
logger = _fastmcp_logger if _fastmcp_logger is not None else setup_logger()
|
||||
else:
|
||||
# Fallback to our custom logger if FastMCP is not available
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
# Get the script directory (config module location)
|
||||
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/
|
||||
@@ -18,10 +19,10 @@ PROJECT_ROOT = UNRAID_MCP_DIR.parent # /home/user/code/unraid-mcp/
|
||||
# Load environment variables from .env file
|
||||
# In container: First try /app/.env.local (mounted), then project root .env
|
||||
dotenv_paths = [
|
||||
Path('/app/.env.local'), # Container mount point
|
||||
PROJECT_ROOT / '.env.local', # Project root .env.local
|
||||
PROJECT_ROOT / '.env', # Project root .env
|
||||
UNRAID_MCP_DIR / '.env' # Local .env in unraid_mcp/
|
||||
Path("/app/.env.local"), # Container mount point
|
||||
PROJECT_ROOT / ".env.local", # Project root .env.local
|
||||
PROJECT_ROOT / ".env", # Project root .env
|
||||
UNRAID_MCP_DIR / ".env" # Local .env in unraid_mcp/
|
||||
]
|
||||
|
||||
for dotenv_path in dotenv_paths:
|
||||
@@ -51,7 +52,7 @@ else: # Path to CA bundle
|
||||
UNRAID_VERIFY_SSL = raw_verify_ssl
|
||||
|
||||
# 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")
|
||||
LOGS_DIR = Path("/tmp")
|
||||
LOG_FILE_PATH = LOGS_DIR / LOG_FILE_NAME
|
||||
@@ -61,8 +62,8 @@ LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# HTTP Client Configuration
|
||||
TIMEOUT_CONFIG = {
|
||||
'default': 30,
|
||||
'disk_operations': 90, # Longer timeout for SMART data queries
|
||||
"default": 30,
|
||||
"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()
|
||||
|
||||
return {
|
||||
'api_url_configured': bool(UNRAID_API_URL),
|
||||
'api_url_preview': UNRAID_API_URL[:20] + '...' if UNRAID_API_URL else None,
|
||||
'api_key_configured': bool(UNRAID_API_KEY),
|
||||
'server_host': UNRAID_MCP_HOST,
|
||||
'server_port': UNRAID_MCP_PORT,
|
||||
'transport': UNRAID_MCP_TRANSPORT,
|
||||
'ssl_verify': UNRAID_VERIFY_SSL,
|
||||
'log_level': LOG_LEVEL_STR,
|
||||
'log_file': str(LOG_FILE_PATH),
|
||||
'config_valid': is_valid,
|
||||
'missing_config': missing if not is_valid else None
|
||||
"api_url_configured": bool(UNRAID_API_URL),
|
||||
"api_url_preview": UNRAID_API_URL[:20] + "..." if UNRAID_API_URL else None,
|
||||
"api_key_configured": bool(UNRAID_API_KEY),
|
||||
"server_host": UNRAID_MCP_HOST,
|
||||
"server_port": UNRAID_MCP_PORT,
|
||||
"transport": UNRAID_MCP_TRANSPORT,
|
||||
"ssl_verify": UNRAID_VERIFY_SSL,
|
||||
"log_level": LOG_LEVEL_STR,
|
||||
"log_file": str(LOG_FILE_PATH),
|
||||
"config_valid": is_valid,
|
||||
"missing_config": missing if not is_valid else None
|
||||
}
|
||||
|
||||
@@ -20,14 +20,21 @@ from ..config.settings import (
|
||||
)
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
# Sensitive keys to redact from debug logs
|
||||
_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:
|
||||
"""Recursively redact sensitive values from nested dicts/lists."""
|
||||
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):
|
||||
return [_redact_sensitive(item) for item in obj]
|
||||
return obj
|
||||
@@ -35,7 +42,25 @@ def _redact_sensitive(obj: Any) -> Any:
|
||||
|
||||
# HTTP timeout configuration
|
||||
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)
|
||||
_http_client: httpx.AsyncClient | None = None
|
||||
@@ -55,26 +80,54 @@ def is_idempotent_error(error_message: str, operation: str) -> bool:
|
||||
error_lower = error_message.lower()
|
||||
|
||||
# Docker container operation patterns
|
||||
if operation == 'start':
|
||||
if operation == "start":
|
||||
return (
|
||||
'already started' in error_lower or
|
||||
'container already running' in error_lower or
|
||||
'http code 304' in error_lower
|
||||
"already started" in error_lower or
|
||||
"container already running" in error_lower or
|
||||
"http code 304" in error_lower
|
||||
)
|
||||
elif operation == 'stop':
|
||||
if operation == "stop":
|
||||
return (
|
||||
'already stopped' in error_lower or
|
||||
'container already stopped' in error_lower or
|
||||
'container not running' in error_lower or
|
||||
'http code 304' in error_lower
|
||||
"already stopped" in error_lower or
|
||||
"container already stopped" in error_lower or
|
||||
"container not running" in error_lower or
|
||||
"http code 304" in error_lower
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def _create_http_client() -> httpx.AsyncClient:
|
||||
"""Create a new HTTP client instance with connection pooling.
|
||||
|
||||
Returns:
|
||||
A new AsyncClient configured for Unraid API communication
|
||||
"""
|
||||
return httpx.AsyncClient(
|
||||
# Connection pool settings
|
||||
limits=httpx.Limits(
|
||||
max_keepalive_connections=20,
|
||||
max_connections=100,
|
||||
keepalive_expiry=30.0
|
||||
),
|
||||
# Default timeout (can be overridden per-request)
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
# SSL verification
|
||||
verify=UNRAID_VERIFY_SSL,
|
||||
# Connection pooling headers
|
||||
headers={
|
||||
"Connection": "keep-alive",
|
||||
"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
|
||||
"""
|
||||
@@ -82,26 +135,21 @@ async def get_http_client() -> httpx.AsyncClient:
|
||||
|
||||
async with _client_lock:
|
||||
if _http_client is None or _http_client.is_closed:
|
||||
_http_client = httpx.AsyncClient(
|
||||
# Connection pool settings
|
||||
limits=httpx.Limits(
|
||||
max_keepalive_connections=20,
|
||||
max_connections=100,
|
||||
keepalive_expiry=30.0
|
||||
),
|
||||
# Default timeout (can be overridden per-request)
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
# SSL verification
|
||||
verify=UNRAID_VERIFY_SSL,
|
||||
# Connection pooling headers
|
||||
headers={
|
||||
"Connection": "keep-alive",
|
||||
"User-Agent": f"UnraidMCPServer/{VERSION}"
|
||||
}
|
||||
)
|
||||
_http_client = await _create_http_client()
|
||||
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:
|
||||
@@ -175,12 +223,12 @@ async def make_graphql_request(
|
||||
response.raise_for_status() # Raise an exception for HTTP error codes 4xx/5xx
|
||||
|
||||
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"]])
|
||||
|
||||
# Check if this is an idempotent error that should be treated as success
|
||||
if operation_context and operation_context.get('operation'):
|
||||
operation = operation_context['operation']
|
||||
if operation_context and operation_context.get("operation"):
|
||||
operation = operation_context["operation"]
|
||||
if is_idempotent_error(error_details, operation):
|
||||
logger.warning(f"Idempotent operation '{operation}' - treating as success: {error_details}")
|
||||
# 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
|
||||
except httpx.RequestError as 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:
|
||||
logger.error(f"Failed to decode JSON response: {e}")
|
||||
raise ToolError(f"Invalid JSON response from Unraid API: {str(e)}") 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
|
||||
raise ToolError(f"Invalid JSON response from Unraid API: {e!s}") from e
|
||||
|
||||
@@ -6,6 +6,7 @@ the modular server implementation from unraid_mcp.server.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
|
||||
async def shutdown_cleanup() -> None:
|
||||
@@ -26,16 +27,20 @@ def main() -> None:
|
||||
print("\nServer stopped by user")
|
||||
try:
|
||||
asyncio.run(shutdown_cleanup())
|
||||
except RuntimeError:
|
||||
# Event loop already closed, skip cleanup
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
if "event loop is closed" in str(e).lower() or "no running event loop" in str(e).lower():
|
||||
pass # Expected during shutdown
|
||||
else:
|
||||
print(f"WARNING: Unexpected error during cleanup: {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Server failed to start: {e}")
|
||||
try:
|
||||
asyncio.run(shutdown_cleanup())
|
||||
except RuntimeError:
|
||||
# Event loop already closed, skip cleanup
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
if "event loop is closed" in str(e).lower() or "no running event loop" in str(e).lower():
|
||||
pass # Expected during shutdown
|
||||
else:
|
||||
print(f"WARNING: Unexpected error during cleanup: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from .tools.storage import register_storage_tool
|
||||
from .tools.users import register_users_tool
|
||||
from .tools.virtualization import register_vm_tool
|
||||
|
||||
|
||||
# Initialize FastMCP instance
|
||||
mcp = FastMCP(
|
||||
name="Unraid MCP Server",
|
||||
@@ -48,16 +49,20 @@ def register_all_modules() -> None:
|
||||
logger.info("Subscription resources registered")
|
||||
|
||||
# Register all 10 consolidated tools
|
||||
register_info_tool(mcp)
|
||||
register_array_tool(mcp)
|
||||
register_storage_tool(mcp)
|
||||
register_docker_tool(mcp)
|
||||
register_vm_tool(mcp)
|
||||
register_notifications_tool(mcp)
|
||||
register_rclone_tool(mcp)
|
||||
register_users_tool(mcp)
|
||||
register_keys_tool(mcp)
|
||||
register_health_tool(mcp)
|
||||
registrars = [
|
||||
register_info_tool,
|
||||
register_array_tool,
|
||||
register_storage_tool,
|
||||
register_docker_tool,
|
||||
register_vm_tool,
|
||||
register_notifications_tool,
|
||||
register_rclone_tool,
|
||||
register_users_tool,
|
||||
register_keys_tool,
|
||||
register_health_tool,
|
||||
]
|
||||
for registrar in registrars:
|
||||
registrar(mcp)
|
||||
|
||||
logger.info("All 10 tools registered successfully - Server ready!")
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ development and debugging purposes.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import ssl
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -16,10 +15,11 @@ from fastmcp import FastMCP
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
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 .manager import subscription_manager
|
||||
from .resources import ensure_subscriptions_started
|
||||
from .utils import build_ws_ssl_context
|
||||
|
||||
|
||||
def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
@@ -31,8 +31,8 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
|
||||
@mcp.tool()
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -49,15 +49,7 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
raise ToolError("UNRAID_API_URL is not configured")
|
||||
ws_url = UNRAID_API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/graphql"
|
||||
|
||||
# Build SSL context for wss:// connections
|
||||
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()
|
||||
ssl_context = build_ws_ssl_context(ws_url)
|
||||
|
||||
# Test connection
|
||||
async with websockets.connect(
|
||||
@@ -104,7 +96,7 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
"query_tested": subscription_query
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
return {
|
||||
"success": True,
|
||||
"response": "No immediate response (subscriptions may only send data on changes)",
|
||||
@@ -121,8 +113,8 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
|
||||
@mcp.tool()
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@@ -163,14 +155,14 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
|
||||
# Calculate WebSocket URL
|
||||
if UNRAID_API_URL:
|
||||
if UNRAID_API_URL.startswith('https://'):
|
||||
ws_url = 'wss://' + UNRAID_API_URL[len('https://'):]
|
||||
elif UNRAID_API_URL.startswith('http://'):
|
||||
ws_url = 'ws://' + UNRAID_API_URL[len('http://'):]
|
||||
if UNRAID_API_URL.startswith("https://"):
|
||||
ws_url = "wss://" + UNRAID_API_URL[len("https://"):]
|
||||
elif UNRAID_API_URL.startswith("http://"):
|
||||
ws_url = "ws://" + UNRAID_API_URL[len("http://"):]
|
||||
else:
|
||||
ws_url = UNRAID_API_URL
|
||||
if not ws_url.endswith('/graphql'):
|
||||
ws_url = ws_url.rstrip('/') + '/graphql'
|
||||
if not ws_url.endswith("/graphql"):
|
||||
ws_url = ws_url.rstrip("/") + "/graphql"
|
||||
diagnostic_info["environment"]["websocket_url"] = ws_url
|
||||
|
||||
# Analyze issues
|
||||
@@ -222,6 +214,6 @@ def register_diagnostic_tools(mcp: FastMCP) -> None:
|
||||
|
||||
except Exception as 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")
|
||||
|
||||
@@ -8,7 +8,6 @@ error handling, reconnection logic, and authentication.
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -16,8 +15,9 @@ import websockets
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
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 .utils import build_ws_ssl_context
|
||||
|
||||
|
||||
class SubscriptionManager:
|
||||
@@ -141,28 +141,20 @@ class SubscriptionManager:
|
||||
if not UNRAID_API_URL:
|
||||
raise ValueError("UNRAID_API_URL is not configured")
|
||||
|
||||
if UNRAID_API_URL.startswith('https://'):
|
||||
ws_url = 'wss://' + UNRAID_API_URL[len('https://'):]
|
||||
elif UNRAID_API_URL.startswith('http://'):
|
||||
ws_url = 'ws://' + UNRAID_API_URL[len('http://'):]
|
||||
if UNRAID_API_URL.startswith("https://"):
|
||||
ws_url = "wss://" + UNRAID_API_URL[len("https://"):]
|
||||
elif UNRAID_API_URL.startswith("http://"):
|
||||
ws_url = "ws://" + UNRAID_API_URL[len("http://"):]
|
||||
else:
|
||||
ws_url = UNRAID_API_URL
|
||||
|
||||
if not ws_url.endswith('/graphql'):
|
||||
ws_url = ws_url.rstrip('/') + '/graphql'
|
||||
if not ws_url.endswith("/graphql"):
|
||||
ws_url = ws_url.rstrip("/") + "/graphql"
|
||||
|
||||
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'}")
|
||||
|
||||
# Build SSL context for wss:// connections
|
||||
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()
|
||||
ssl_context = build_ws_ssl_context(ws_url)
|
||||
|
||||
# Connection with timeout
|
||||
connect_timeout = 10
|
||||
@@ -213,7 +205,7 @@ class SubscriptionManager:
|
||||
init_data = json.loads(init_raw)
|
||||
logger.debug(f"[PROTOCOL:{subscription_name}] Received init response: {init_data.get('type')}")
|
||||
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}...")
|
||||
self.last_error[subscription_name] = f"Invalid JSON in init response: {e}"
|
||||
break
|
||||
@@ -223,7 +215,7 @@ class SubscriptionManager:
|
||||
logger.info(f"[PROTOCOL:{subscription_name}] Connection acknowledged successfully")
|
||||
self.connection_states[subscription_name] = "authenticated"
|
||||
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}")
|
||||
self.last_error[subscription_name] = f"Authentication error: {error_payload}"
|
||||
self.connection_states[subscription_name] = "auth_failed"
|
||||
@@ -259,7 +251,7 @@ class SubscriptionManager:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
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}")
|
||||
|
||||
@@ -288,7 +280,7 @@ class SubscriptionManager:
|
||||
await websocket.send(json.dumps({"type": "pong"}))
|
||||
|
||||
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}")
|
||||
self.last_error[subscription_name] = f"Subscription error: {error_payload}"
|
||||
self.connection_states[subscription_name] = "error"
|
||||
@@ -305,15 +297,15 @@ class SubscriptionManager:
|
||||
logger.debug(f"[PROTOCOL:{subscription_name}] Unhandled message type: {message_type}")
|
||||
|
||||
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}] JSON decode error: {e}")
|
||||
except Exception as 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}...")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
error_msg = "Connection or authentication timeout"
|
||||
logger.error(f"[WEBSOCKET:{subscription_name}] {error_msg}")
|
||||
self.last_error[subscription_name] = error_msg
|
||||
@@ -353,9 +345,8 @@ class SubscriptionManager:
|
||||
age_seconds = (datetime.now() - data.last_updated).total_seconds()
|
||||
logger.debug(f"[RESOURCE:{resource_name}] Data found, age: {age_seconds:.1f}s")
|
||||
return data.data
|
||||
else:
|
||||
logger.debug(f"[RESOURCE:{resource_name}] No data available")
|
||||
return None
|
||||
logger.debug(f"[RESOURCE:{resource_name}] No data available")
|
||||
return None
|
||||
|
||||
def list_active_subscriptions(self) -> list[str]:
|
||||
"""List all active subscriptions."""
|
||||
|
||||
@@ -13,6 +13,7 @@ from fastmcp import FastMCP
|
||||
from ..config.logging import logger
|
||||
from .manager import subscription_manager
|
||||
|
||||
|
||||
# Global flag to track subscription startup
|
||||
_subscriptions_started = False
|
||||
|
||||
|
||||
27
unraid_mcp/subscriptions/utils.py
Normal file
27
unraid_mcp/subscriptions/utils.py
Normal 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
|
||||
@@ -12,9 +12,10 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"parity_history": """
|
||||
query GetParityHistory {
|
||||
"parity_status": """
|
||||
query GetParityStatus {
|
||||
array { parityCheckStatus { progress speed errors } }
|
||||
}
|
||||
""",
|
||||
@@ -80,10 +81,11 @@ MUTATIONS: dict[str, str] = {
|
||||
|
||||
DESTRUCTIVE_ACTIONS = {"start", "stop", "shutdown", "reboot"}
|
||||
DISK_ACTIONS = {"mount_disk", "unmount_disk", "clear_stats"}
|
||||
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
|
||||
|
||||
ARRAY_ACTIONS = Literal[
|
||||
"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",
|
||||
"shutdown", "reboot",
|
||||
]
|
||||
@@ -108,16 +110,15 @@ def register_array_tool(mcp: FastMCP) -> None:
|
||||
parity_pause - Pause running parity check
|
||||
parity_resume - Resume paused 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)
|
||||
unmount_disk - Unmount an array disk (requires disk_id)
|
||||
clear_stats - Clear disk statistics (requires disk_id)
|
||||
shutdown - Shut down 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:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
|
||||
if action not in ALL_ACTIONS:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
|
||||
|
||||
if action in DESTRUCTIVE_ACTIONS and not confirm:
|
||||
raise ToolError(
|
||||
@@ -156,6 +157,6 @@ def register_array_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -13,6 +13,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"list": """
|
||||
query ListDockerContainers {
|
||||
@@ -98,7 +99,8 @@ MUTATIONS: dict[str, str] = {
|
||||
}
|
||||
|
||||
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[
|
||||
"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_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(
|
||||
@@ -175,6 +177,7 @@ def register_docker_tool(mcp: FastMCP) -> None:
|
||||
action: DOCKER_ACTIONS,
|
||||
container_id: str | None = None,
|
||||
network_id: str | None = None,
|
||||
*,
|
||||
confirm: bool = False,
|
||||
tail_lines: int = 100,
|
||||
) -> dict[str, Any]:
|
||||
@@ -197,14 +200,13 @@ def register_docker_tool(mcp: FastMCP) -> None:
|
||||
port_conflicts - Check for port conflicts
|
||||
check_updates - Check which containers have updates available
|
||||
"""
|
||||
all_actions = set(QUERIES) | set(MUTATIONS) | {"restart"}
|
||||
if action not in all_actions:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
|
||||
if action not in ALL_ACTIONS:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
|
||||
|
||||
if action in DESTRUCTIVE_ACTIONS and not confirm:
|
||||
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")
|
||||
|
||||
if action == "network_details" and not network_id:
|
||||
@@ -327,6 +329,6 @@ def register_docker_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -21,12 +21,24 @@ from ..config.settings import (
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
HEALTH_ACTIONS = Literal["check", "test_connection", "diagnose"]
|
||||
|
||||
# Severity ordering: only upgrade, never downgrade
|
||||
_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:
|
||||
"""Register the unraid_health tool with the FastMCP instance."""
|
||||
|
||||
@@ -71,7 +83,7 @@ def register_health_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -108,15 +120,9 @@ async def _comprehensive_check() -> dict[str, Any]:
|
||||
|
||||
health_info: dict[str, Any] = {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
"api_latency_ms": api_latency,
|
||||
"server": {
|
||||
"name": "Unraid MCP Server",
|
||||
"version": VERSION,
|
||||
"transport": UNRAID_MCP_TRANSPORT,
|
||||
"host": UNRAID_MCP_HOST,
|
||||
"port": UNRAID_MCP_PORT,
|
||||
},
|
||||
"server": _server_info(),
|
||||
}
|
||||
|
||||
if not data:
|
||||
@@ -201,15 +207,9 @@ async def _comprehensive_check() -> dict[str, Any]:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
"error": str(e),
|
||||
"server": {
|
||||
"name": "Unraid MCP Server",
|
||||
"version": VERSION,
|
||||
"transport": UNRAID_MCP_TRANSPORT,
|
||||
"host": UNRAID_MCP_HOST,
|
||||
"port": UNRAID_MCP_PORT,
|
||||
},
|
||||
"server": _server_info(),
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ async def _diagnose_subscriptions() -> dict[str, Any]:
|
||||
connection_issues: list[dict[str, Any]] = []
|
||||
|
||||
diagnostic_info: dict[str, Any] = {
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
"environment": {
|
||||
"auto_start_enabled": subscription_manager.auto_start_enabled,
|
||||
"max_reconnect_attempts": subscription_manager.max_reconnect_attempts,
|
||||
@@ -258,7 +258,7 @@ async def _diagnose_subscriptions() -> dict[str, Any]:
|
||||
except ImportError:
|
||||
return {
|
||||
"error": "Subscription modules not available",
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
}
|
||||
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
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
# Pre-built queries keyed by action name
|
||||
QUERIES: dict[str, str] = {
|
||||
"overview": """
|
||||
@@ -162,6 +163,10 @@ INFO_ACTIONS = Literal[
|
||||
"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]:
|
||||
"""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"]
|
||||
summary["cpu"] = (
|
||||
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"):
|
||||
@@ -227,27 +232,31 @@ def _analyze_disk_health(disks: list[dict[str, Any]]) -> dict[str, int]:
|
||||
return counts
|
||||
|
||||
|
||||
def _format_kb(k: Any) -> str:
|
||||
"""Format kilobyte values into human-readable sizes."""
|
||||
if k is None:
|
||||
return "N/A"
|
||||
try:
|
||||
k = int(k)
|
||||
except (ValueError, TypeError):
|
||||
return "N/A"
|
||||
if k >= 1024 * 1024 * 1024:
|
||||
return f"{k / (1024 * 1024 * 1024):.2f} TB"
|
||||
if k >= 1024 * 1024:
|
||||
return f"{k / (1024 * 1024):.2f} GB"
|
||||
if k >= 1024:
|
||||
return f"{k / 1024:.2f} MB"
|
||||
return f"{k} KB"
|
||||
|
||||
|
||||
def _process_array_status(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Process raw array data into summary + details."""
|
||||
|
||||
def format_kb(k: Any) -> str:
|
||||
if k is None:
|
||||
return "N/A"
|
||||
k = int(k)
|
||||
if k >= 1024 * 1024 * 1024:
|
||||
return f"{k / (1024 * 1024 * 1024):.2f} TB"
|
||||
if k >= 1024 * 1024:
|
||||
return f"{k / (1024 * 1024):.2f} GB"
|
||||
if k >= 1024:
|
||||
return f"{k / 1024:.2f} MB"
|
||||
return f"{k} KB"
|
||||
|
||||
summary: dict[str, Any] = {"state": raw.get("state")}
|
||||
if raw.get("capacity") and raw["capacity"].get("kilobytes"):
|
||||
kb = raw["capacity"]["kilobytes"]
|
||||
summary["capacity_total"] = format_kb(kb.get("total"))
|
||||
summary["capacity_used"] = format_kb(kb.get("used"))
|
||||
summary["capacity_free"] = format_kb(kb.get("free"))
|
||||
summary["capacity_total"] = _format_kb(kb.get("total"))
|
||||
summary["capacity_used"] = _format_kb(kb.get("used"))
|
||||
summary["capacity_free"] = _format_kb(kb.get("free"))
|
||||
|
||||
summary["num_parity_disks"] = len(raw.get("parities", []))
|
||||
summary["num_data_disks"] = len(raw.get("disks", []))
|
||||
@@ -320,81 +329,73 @@ def register_info_tool(mcp: FastMCP) -> None:
|
||||
if action == "ups_device":
|
||||
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:
|
||||
logger.info(f"Executing unraid_info action={action}")
|
||||
data = await make_graphql_request(query, variables)
|
||||
|
||||
# Action-specific response processing
|
||||
# Special-case actions with custom processing
|
||||
if action == "overview":
|
||||
raw = data.get("info", {})
|
||||
raw = data.get("info") or {}
|
||||
if not raw:
|
||||
raise ToolError("No system info returned from Unraid API")
|
||||
return _process_system_info(raw)
|
||||
|
||||
if action == "array":
|
||||
raw = data.get("array", {})
|
||||
raw = data.get("array") or {}
|
||||
if not raw:
|
||||
raise ToolError("No array information returned from Unraid API")
|
||||
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":
|
||||
info = data.get("info", {})
|
||||
return dict(info.get("display", {}))
|
||||
|
||||
if action == "config":
|
||||
return dict(data.get("config", {}))
|
||||
info = data.get("info") or {}
|
||||
return dict(info.get("display") or {})
|
||||
|
||||
if action == "online":
|
||||
return {"online": data.get("online")}
|
||||
|
||||
if action == "owner":
|
||||
return dict(data.get("owner", {}))
|
||||
|
||||
if action == "settings":
|
||||
settings = data.get("settings", {})
|
||||
if settings and settings.get("unified"):
|
||||
values = settings["unified"].get("values", {})
|
||||
return dict(values) if isinstance(values, dict) else {"raw": values}
|
||||
return {}
|
||||
settings = data.get("settings") or {}
|
||||
if not settings:
|
||||
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}
|
||||
|
||||
if action == "server":
|
||||
return data
|
||||
|
||||
if action == "servers":
|
||||
servers = data.get("servers", [])
|
||||
return {"servers": list(servers) if isinstance(servers, list) else []}
|
||||
# Simple dict-returning actions
|
||||
if action in dict_actions:
|
||||
return dict(data.get(dict_actions[action]) or {})
|
||||
|
||||
if action == "flash":
|
||||
return dict(data.get("flash", {}))
|
||||
|
||||
if action == "ups_devices":
|
||||
devices = data.get("upsDevices", [])
|
||||
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", {}))
|
||||
# List-wrapped actions
|
||||
if action in list_actions:
|
||||
response_key, output_key = list_actions[action]
|
||||
items = data.get(response_key) or []
|
||||
return {output_key: list(items) if isinstance(items, list) else []}
|
||||
|
||||
raise ToolError(f"Unhandled action '{action}' — this is a bug")
|
||||
|
||||
@@ -402,6 +403,6 @@ def register_info_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"list": """
|
||||
query ListApiKeys {
|
||||
@@ -90,7 +91,7 @@ def register_keys_tool(mcp: FastMCP) -> None:
|
||||
if not key_id:
|
||||
raise ToolError("key_id is required for 'get' action")
|
||||
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 not name:
|
||||
@@ -144,6 +145,6 @@ def register_keys_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"overview": """
|
||||
query GetNotificationsOverview {
|
||||
@@ -124,8 +125,8 @@ def register_notifications_tool(mcp: FastMCP) -> None:
|
||||
|
||||
if action == "overview":
|
||||
data = await make_graphql_request(QUERIES["overview"])
|
||||
notifications = data.get("notifications", {})
|
||||
return dict(notifications.get("overview", {}))
|
||||
notifications = data.get("notifications") or {}
|
||||
return dict(notifications.get("overview") or {})
|
||||
|
||||
if action == "list":
|
||||
filter_vars: dict[str, Any] = {
|
||||
@@ -200,6 +201,6 @@ def register_notifications_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"list_remotes": """
|
||||
query ListRCloneRemotes {
|
||||
@@ -39,6 +40,7 @@ MUTATIONS: dict[str, str] = {
|
||||
}
|
||||
|
||||
DESTRUCTIVE_ACTIONS = {"delete_remote"}
|
||||
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
|
||||
|
||||
RCLONE_ACTIONS = Literal[
|
||||
"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)
|
||||
delete_remote - Delete a remote (requires name, confirm=True)
|
||||
"""
|
||||
all_actions = set(QUERIES) | set(MUTATIONS)
|
||||
if action not in all_actions:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(all_actions)}")
|
||||
if action not in ALL_ACTIONS:
|
||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
|
||||
|
||||
if action in DESTRUCTIVE_ACTIONS and not confirm:
|
||||
raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.")
|
||||
@@ -129,6 +130,6 @@ def register_rclone_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -4,7 +4,7 @@ Provides the `unraid_storage` tool with 6 actions for shares, physical disks,
|
||||
unassigned devices, log files, and log content retrieval.
|
||||
"""
|
||||
|
||||
import posixpath
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastmcp import FastMCP
|
||||
@@ -102,8 +102,8 @@ def register_storage_tool(mcp: FastMCP) -> None:
|
||||
if action == "logs":
|
||||
if not log_path:
|
||||
raise ToolError("log_path is required for 'logs' action")
|
||||
# Normalize path to prevent traversal attacks (e.g. /var/log/../../etc/shadow)
|
||||
normalized = posixpath.normpath(log_path)
|
||||
# Resolve path to prevent traversal attacks (e.g. /var/log/../../etc/shadow)
|
||||
normalized = str(Path(log_path).resolve())
|
||||
if not any(normalized.startswith(p) for p in _ALLOWED_LOG_PREFIXES):
|
||||
raise ToolError(
|
||||
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"),
|
||||
"size_formatted": format_bytes(raw.get("size")),
|
||||
"temperature": (
|
||||
f"{raw.get('temperature')}C"
|
||||
if raw.get("temperature")
|
||||
f"{raw['temperature']}\u00b0C"
|
||||
if raw.get("temperature") is not None
|
||||
else "N/A"
|
||||
),
|
||||
}
|
||||
@@ -159,7 +159,7 @@ def register_storage_tool(mcp: FastMCP) -> None:
|
||||
return {"log_files": list(files) if isinstance(files, list) else []}
|
||||
|
||||
if action == "logs":
|
||||
return dict(data.get("logFile", {}))
|
||||
return dict(data.get("logFile") or {})
|
||||
|
||||
raise ToolError(f"Unhandled action '{action}' — this is a bug")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"me": """
|
||||
query GetMe {
|
||||
@@ -158,6 +159,6 @@ def register_users_tool(mcp: FastMCP) -> None:
|
||||
raise
|
||||
except Exception as e:
|
||||
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")
|
||||
|
||||
@@ -12,17 +12,13 @@ from ..config.logging import logger
|
||||
from ..core.client import make_graphql_request
|
||||
from ..core.exceptions import ToolError
|
||||
|
||||
|
||||
QUERIES: dict[str, str] = {
|
||||
"list": """
|
||||
query ListVMs {
|
||||
vms { id domains { id name state uuid } }
|
||||
}
|
||||
""",
|
||||
"details": """
|
||||
query GetVmDetails {
|
||||
vms { domains { id name state uuid } }
|
||||
}
|
||||
""",
|
||||
}
|
||||
|
||||
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] = {
|
||||
"start": "start",
|
||||
"stop": "stop",
|
||||
"pause": "pause",
|
||||
"resume": "resume",
|
||||
"force_stop": "forceStop",
|
||||
"reboot": "reboot",
|
||||
"reset": "reset",
|
||||
}
|
||||
|
||||
DESTRUCTIVE_ACTIONS = {"force_stop", "reset"}
|
||||
@@ -90,7 +80,7 @@ def register_vm_tool(mcp: FastMCP) -> None:
|
||||
reboot - Reboot a VM (requires vm_id)
|
||||
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:
|
||||
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:
|
||||
logger.info(f"Executing unraid_vm action={action}")
|
||||
|
||||
if action == "list":
|
||||
if action in ("list", "details"):
|
||||
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"):
|
||||
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:
|
||||
if (
|
||||
vm.get("uuid") == vm_id
|
||||
or vm.get("id") == vm_id
|
||||
or vm.get("name") == vm_id
|
||||
):
|
||||
return dict(vm) if isinstance(vm, dict) else {}
|
||||
return dict(vm)
|
||||
available = [
|
||||
f"{v.get('name')} (UUID: {v.get('uuid')})" for v in vms
|
||||
]
|
||||
raise ToolError(
|
||||
f"VM '{vm_id}' not found. Available: {', '.join(available)}"
|
||||
)
|
||||
raise ToolError("No VM data returned from server")
|
||||
if action == "details":
|
||||
raise ToolError("No VM data returned from server")
|
||||
return {"vms": []}
|
||||
|
||||
# Mutations
|
||||
if action in MUTATIONS:
|
||||
data = await make_graphql_request(
|
||||
MUTATIONS[action], {"id": vm_id}
|
||||
)
|
||||
field = _MUTATION_FIELDS[action]
|
||||
field = _MUTATION_FIELDS.get(action, action)
|
||||
if data.get("vm") and field in data["vm"]:
|
||||
return {
|
||||
"success": data["vm"][field],
|
||||
|
||||
553
uv.lock
generated
553
uv.lock
generated
@@ -1,6 +1,6 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -16,7 +16,6 @@ name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "build"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "os_name == 'nt'" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
|
||||
{ name = "packaging" },
|
||||
{ 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" }
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -213,28 +132,6 @@ version = "3.4.3"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -298,31 +195,6 @@ version = "7.13.3"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
toml = [
|
||||
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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" }
|
||||
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/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/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]]
|
||||
@@ -470,7 +328,6 @@ dependencies = [
|
||||
{ name = "docstring-parser", marker = "python_full_version < '4'" },
|
||||
{ name = "rich" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -593,20 +450,6 @@ version = "0.7.1"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
@@ -721,9 +552,6 @@ wheels = [
|
||||
name = "jaraco-context"
|
||||
version = "6.1.0"
|
||||
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" }
|
||||
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" },
|
||||
@@ -797,7 +625,6 @@ name = "keyring"
|
||||
version = "25.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
|
||||
{ name = "jaraco-classes" },
|
||||
{ name = "jaraco-context" },
|
||||
{ name = "jaraco-functools" },
|
||||
@@ -816,18 +643,6 @@ version = "1.12.0"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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/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/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]]
|
||||
@@ -873,28 +687,6 @@ version = "3.0.3"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "nh3"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pluggy"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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/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/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]]
|
||||
@@ -1312,12 +1032,10 @@ version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -1329,7 +1047,6 @@ name = "pytest-asyncio"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
@@ -1343,7 +1060,7 @@ name = "pytest-cov"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage", extra = ["toml"] },
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
@@ -1393,12 +1101,6 @@ name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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/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" },
|
||||
@@ -1425,24 +1127,6 @@ version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/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/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" },
|
||||
@@ -1591,35 +1275,6 @@ version = "0.27.1"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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/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/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]]
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "twine"
|
||||
version = "6.2.0"
|
||||
@@ -1930,46 +1521,8 @@ dependencies = [
|
||||
{ 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]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "build" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -1982,37 +1535,18 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
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 = "fastmcp", specifier = ">=2.11.2" },
|
||||
{ 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 = "pytz", specifier = ">=2025.2" },
|
||||
{ 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 = "websockets", specifier = ">=13.1,<14.0" },
|
||||
]
|
||||
provides-extras = ["dev", "test", "lint", "types", "all"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
{ name = "build", specifier = ">=1.2.2" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||
@@ -2039,7 +1573,6 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -2063,18 +1596,6 @@ version = "0.22.1"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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" }
|
||||
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/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" },
|
||||
@@ -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/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/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]]
|
||||
@@ -2210,28 +1698,6 @@ version = "13.1"
|
||||
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" }
|
||||
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/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" },
|
||||
@@ -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/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/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" },
|
||||
]
|
||||
|
||||
@@ -2274,12 +1734,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user