From f61f49913d0f15dc40f06afd3d02c6fd94502a32 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:01:32 -0400 Subject: [PATCH 01/13] Remove opt/torero-api --- Containerfile | 14 +- opt/torero-api/README.md | 108 - opt/torero-api/pyproject.toml | 217 -- opt/torero-api/scripts/generate_openapi.py | 66 - opt/torero-api/scripts/run_api.py | 20 - opt/torero-api/scripts/torero_api_ctl.sh | 244 --- opt/torero-api/torero_api/__init__.py | 8 - opt/torero-api/torero_api/__main__.py | 277 --- opt/torero-api/torero_api/api/__init__.py | 21 - opt/torero-api/torero_api/api/v1/__init__.py | 17 - .../torero_api/api/v1/endpoints/__init__.py | 17 - .../torero_api/api/v1/endpoints/database.py | 251 --- .../torero_api/api/v1/endpoints/decorators.py | 253 --- .../torero_api/api/v1/endpoints/execution.py | 330 --- .../torero_api/api/v1/endpoints/registries.py | 184 -- .../api/v1/endpoints/repositories.py | 255 --- .../torero_api/api/v1/endpoints/secrets.py | 257 --- .../torero_api/api/v1/endpoints/services.py | 354 ---- opt/torero-api/torero_api/core/__init__.py | 31 - .../torero_api/core/torero_executor.py | 1178 ----------- opt/torero-api/torero_api/models/__init__.py | 27 - opt/torero-api/torero_api/models/common.py | 64 - opt/torero-api/torero_api/models/database.py | 51 - opt/torero-api/torero_api/models/decorator.py | 63 - opt/torero-api/torero_api/models/execution.py | 44 - opt/torero-api/torero_api/models/registry.py | 48 - .../torero_api/models/repository.py | 59 - opt/torero-api/torero_api/models/secret.py | 60 - opt/torero-api/torero_api/models/service.py | 80 - opt/torero-api/torero_api/server.py | 255 --- opt/torero-api/uv.lock | 1839 ----------------- opt/torero-mcp/torero_mcp/client.py | 558 ----- 32 files changed, 2 insertions(+), 7248 deletions(-) delete mode 100644 opt/torero-api/README.md delete mode 100644 opt/torero-api/pyproject.toml delete mode 100755 opt/torero-api/scripts/generate_openapi.py delete mode 100755 opt/torero-api/scripts/run_api.py delete mode 100755 opt/torero-api/scripts/torero_api_ctl.sh delete mode 100644 opt/torero-api/torero_api/__init__.py delete mode 100644 opt/torero-api/torero_api/__main__.py delete mode 100644 opt/torero-api/torero_api/api/__init__.py delete mode 100644 opt/torero-api/torero_api/api/v1/__init__.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/__init__.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/database.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/decorators.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/execution.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/registries.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/repositories.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/secrets.py delete mode 100644 opt/torero-api/torero_api/api/v1/endpoints/services.py delete mode 100644 opt/torero-api/torero_api/core/__init__.py delete mode 100644 opt/torero-api/torero_api/core/torero_executor.py delete mode 100644 opt/torero-api/torero_api/models/__init__.py delete mode 100644 opt/torero-api/torero_api/models/common.py delete mode 100644 opt/torero-api/torero_api/models/database.py delete mode 100644 opt/torero-api/torero_api/models/decorator.py delete mode 100644 opt/torero-api/torero_api/models/execution.py delete mode 100644 opt/torero-api/torero_api/models/registry.py delete mode 100644 opt/torero-api/torero_api/models/repository.py delete mode 100644 opt/torero-api/torero_api/models/secret.py delete mode 100644 opt/torero-api/torero_api/models/service.py delete mode 100644 opt/torero-api/torero_api/server.py delete mode 100644 opt/torero-api/uv.lock delete mode 100644 opt/torero-mcp/torero_mcp/client.py diff --git a/Containerfile b/Containerfile index 6024513..3caf8be 100644 --- a/Containerfile +++ b/Containerfile @@ -35,16 +35,10 @@ ENV TORERO_MCP_TRANSPORT_TYPE=sse ENV TORERO_MCP_TRANSPORT_HOST=0.0.0.0 ENV TORERO_MCP_TRANSPORT_PORT=8080 ENV TORERO_MCP_TRANSPORT_PATH=/sse -ENV TORERO_API_BASE_URL=http://localhost:8000 -ENV TORERO_API_TIMEOUT=30 +ENV TORERO_CLI_TIMEOUT=30 ENV TORERO_LOG_LEVEL=INFO -ENV TORERO_MCP_PID_FILE=/tmp/torero-mcp.pid ENV TORERO_MCP_LOG_FILE=/home/admin/.torero-mcp.log -# api server is disabled by default -ENV ENABLE_API=false -ENV API_PORT=8000 - # ui server is disabled by default ENV ENABLE_UI=false ENV UI_PORT=8001 @@ -60,12 +54,11 @@ COPY configure.sh /configure.sh COPY entrypoint.sh /entrypoint.sh # copy torero projects to image -COPY opt/torero-api /opt/torero-api COPY opt/torero-ui /opt/torero-ui COPY opt/torero-mcp /opt/torero-mcp # install Python dependencies at build time -RUN pip install --no-cache-dir -e /opt/torero-api /opt/torero-ui /opt/torero-mcp +RUN pip install --no-cache-dir -e /opt/torero-ui /opt/torero-mcp # set up Django static files at build time ENV DJANGO_SETTINGS_MODULE=torero_ui.settings @@ -85,9 +78,6 @@ RUN chmod +x /configure.sh && /configure.sh && \ # expose ssh port (only used if SSH is enabled) EXPOSE 22 -# expose API port (only used if API is enabled) -EXPOSE 8000 - # expose UI port (only used if UI is enabled) EXPOSE 8001 diff --git a/opt/torero-api/README.md b/opt/torero-api/README.md deleted file mode 100644 index 6374817..0000000 --- a/opt/torero-api/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# torero API Service - -RESTful API service providing programmatic access to torero automation capabilities. - -## Features -- **Service Discovery**: List, filter, and search torero services -- **Service Execution**: Execute Ansible playbooks, Python scripts, and OpenTofu plans -- **Registry Management**: Manage package registries (Ansible Galaxy, PyPI, etc.) -- **Database Operations**: Export and import configurations between torero instances -- **Auto Documentation**: Interactive API docs with OpenAPI/Swagger - -## Configuration - -The API service is configured via environment variables: - -| Variable | Default | Description | -|----------|---------|-------------| -| `ENABLE_API` | `false` | Enable the API service | -| `API_PORT` | `8000` | Port for the API service | -| `API_HOST` | `0.0.0.0` | Host binding for the API | - -## API Endpoints - -### Core Endpoints -- `GET /health` - Health check endpoint -- `GET /api/v1/services` - List all available services -- `POST /api/v1/services/{service_name}/execute` - Execute a service -- `GET /api/v1/services/{service_name}/describe` - Get service details - -### Decorator Endpoints -- `GET /api/v1/decorators` - List all decorators -- `GET /api/v1/decorators/{name}` - Get decorator details - -### Repository Endpoints -- `GET /api/v1/repositories` - List all repositories -- `GET /api/v1/repositories/{name}` - Get repository details - -### Registry Endpoints -- `GET /api/v1/registries` - List all registries -- `POST /api/v1/registries/{name}/packages` - List packages in registry - -### Database Operations -- `POST /api/v1/database/export` - Export torero database -- `POST /api/v1/database/import` - Import torero database - -### Secret Management -- `GET /api/v1/secrets` - List all secrets -- `GET /api/v1/secrets/{name}` - Get secret details -- `POST /api/v1/secrets` - Create a new secret -- `PUT /api/v1/secrets/{name}` - Update a secret -- `DELETE /api/v1/secrets/{name}` - Delete a secret - -## Usage Examples - -### List Services -```bash -curl http://localhost:8000/api/v1/services -``` - -### Execute a Service -```bash -curl -X POST http://localhost:8000/api/v1/services/my-playbook/execute \ - -H "Content-Type: application/json" \ - -d '{"parameters": {"target": "localhost"}}' -``` - -### Filter Services by Type -```bash -curl "http://localhost:8000/api/v1/services?service_type=ansible-playbook" -``` - -### Export Database -```bash -curl -X POST http://localhost:8000/api/v1/database/export \ - -H "Content-Type: application/json" \ - -d '{"output_file": "/tmp/torero-backup.tar.gz"}' -``` - -## Interactive Documentation - -When the API is running, access the interactive documentation at: -- Swagger UI: `http://localhost:8000/docs` -- ReDoc: `http://localhost:8000/redoc` -- OpenAPI Schema: `http://localhost:8000/openapi.json` - -## Development - -### Running Locally -```bash -# from the container root -cd opt/torero-api -python -m torero_api - -# with auto-reload for development -python -m torero_api --reload -``` - -### Running Tests -```bash -# from the container root -./tools.sh --test -``` - -### Generating OpenAPI Schema -```bash -# from the container root -./tools.sh --schema -``` \ No newline at end of file diff --git a/opt/torero-api/pyproject.toml b/opt/torero-api/pyproject.toml deleted file mode 100644 index e5834b7..0000000 --- a/opt/torero-api/pyproject.toml +++ /dev/null @@ -1,217 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "torero-api" -version = "0.1.0" -description = "RESTful API for torero" -authors = [ - { name = "William Collins", email = "opensource@itential.com" } -] -readme = "README.md" -requires-python = ">=3.10,<4.0" -license = {text = "Apache-2.0"} -keywords = ["fastapi", "api", "torero", "automation", "ansible", "opentofu"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Web Environment", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: System :: Systems Administration", - "Topic :: System :: Networking", -] -dependencies = [ - "fastapi>=0.115.0,<0.116.0", - "uvicorn[standard]>=0.29.0,<0.30.0", - "pydantic>=2.0.0,<3.0.0", - "python-multipart>=0.0.20", - "httpx>=0.25.0", - "python-dateutil>=2.8.2", -] - -[project.urls] -"Homepage" = "https://github.com/torerodev/torero-api" -"Bug Tracker" = "https://github.com/torerodev/torero-api/issues" -"Documentation" = "https://github.com/torerodev/torero-api#readme" -"Source Code" = "https://github.com/torerodev/torero-api" -"Changelog" = "https://github.com/torerodev/torero-api/releases" - -[project.scripts] -torero-api = "torero_api.__main__:main" -generate-openapi = "scripts.generate_openapi:generate_openapi_schema" - -[project.optional-dependencies] -yaml = ["PyYAML>=6.0"] -docs = ["PyYAML>=6.0", "mkdocs>=1.5.2", "mkdocs-material>=9.1.21"] -dev = [ - "pytest>=7.4.0", - "pytest-asyncio>=0.21.0", - "pytest-cov>=4.1.0", - "httpx>=0.28.1", - "black>=23.7.0", - "isort>=5.12.0", - "flake8>=6.1.0", - "PyYAML>=6.0", -] -all = ["torero-api[yaml,docs,dev]"] - -[tool.uv] -dev-dependencies = [ - "pytest>=7.4.0", - "pytest-asyncio>=0.21.0", - "pytest-cov>=4.1.0", - "httpx>=0.28.1", - "black>=23.7.0", - "isort>=5.12.0", - "flake8>=6.1.0", - "mypy>=1.5.0", - "PyYAML>=6.0", - "pre-commit>=3.3.3", - "build>=0.10.0", - "twine>=4.0.0", -] - -[tool.hatch.build.targets.wheel] -packages = ["torero_api"] - -[tool.hatch.build] -include = [ - "torero_api/**/*.py", - "torero_api/py.typed", - "README.md", - "LICENSE", -] -exclude = [ - "tests/", - "docs/", - "scripts/", - ".github/", -] - -[tool.hatch.version] -path = "torero_api/__init__.py" - -[tool.pytest.ini_options] -testpaths = ["tests"] -python_files = "test_*.py" -python_functions = "test_*" -python_classes = "Test*" -asyncio_mode = "auto" -filterwarnings = [ - "ignore::PendingDeprecationWarning:starlette.formparsers" -] -addopts = [ - "--cov=torero_api", - "--cov-report=term-missing", - "--cov-report=xml", - "--cov-report=html", - "-v" -] - -[tool.black] -line-length = 88 -target-version = ['py310'] -include = '\.pyi?$' -extend-exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' - -[tool.isort] -profile = "black" -line_length = 88 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -skip_glob = ["**/migrations/*"] - -[tool.flake8] -max-line-length = 88 -extend-ignore = [ - "E203", # whitespace before ':' - "E501", # line too long (handled by black) - "W503", # line break before binary operator -] -exclude = [ - ".git", - "__pycache__", - ".venv", - "build", - "dist", - "*.egg-info", -] -per-file-ignores = [ - "__init__.py:F401", # imported but unused - "tests/*:S101", # use of assert -] - -[tool.mypy] -python_version = "3.10" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -warn_unreachable = true -strict_equality = true -show_error_codes = true - -[[tool.mypy.overrides]] -module = [ - "uvicorn.*", - "fastapi.*", -] -ignore_missing_imports = true - -[tool.coverage.run] -source = ["torero_api"] -omit = [ - "*/tests/*", - "*/test_*", - "*/conftest.py", - "*/__main__.py", -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "class .*\\\\bProtocol\\\\(.*\\\\):", - "@(abc\\\\.)?abstractmethod", -] - -[tool.bandit] -exclude_dirs = ["tests"] -tests = ["B201", "B301"] -skips = ["B101", "B601"] diff --git a/opt/torero-api/scripts/generate_openapi.py b/opt/torero-api/scripts/generate_openapi.py deleted file mode 100755 index 88da995..0000000 --- a/opt/torero-api/scripts/generate_openapi.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate OpenAPI schema file for torero API - -This script generates the OpenAPI schema file that can be committed to the repository -for use by MCP servers, documentation generation, and client SDK generation. -""" - -import json -import os -import sys -from pathlib import Path - -# Add the project root to the Python path -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) - -from torero_api.server import create_app - -def generate_openapi_schema(output_path: str = "openapi.json"): - """ - Generate the OpenAPI schema and save it to a file. - - Args: - output_path: Path where the OpenAPI schema file should be saved - """ - - # Create the FastAPI app - app = create_app() - - # Generate the OpenAPI schema - openapi_schema = app.openapi() - - # Ensure output directory exists - output_file = Path(output_path) - output_file.parent.mkdir(parents=True, exist_ok=True) - - # Write the schema to file with pretty formatting - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(openapi_schema, f, indent=2, ensure_ascii=False) - - print(f"OpenAPI schema generated successfully: {output_file}") - print(f"Schema version: {openapi_schema['info']['version']}") - - # Generate .yaml too! - try: - import yaml - yaml_path = output_file.with_suffix('.yaml') - with open(yaml_path, 'w', encoding='utf-8') as f: - yaml.dump(openapi_schema, f, default_flow_style=False, allow_unicode=True) - print(f"YAML schema also generated: {yaml_path}") - except ImportError: - print("PyYAML not installed - skipping YAML generation") - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Generate OpenAPI schema for torero API") - parser.add_argument( - "-o", "--output", - default="docs/openapi.json", - help="Output path for the OpenAPI schema file (default: docs/openapi.json)" - ) - - args = parser.parse_args() - generate_openapi_schema(args.output) \ No newline at end of file diff --git a/opt/torero-api/scripts/run_api.py b/opt/torero-api/scripts/run_api.py deleted file mode 100755 index 797e48d..0000000 --- a/opt/torero-api/scripts/run_api.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to run the torero API server. - -This script provides a convenient way to start the torero API server -without having to use the "python -m torero_api" command. -""" - -import sys -import os - -# Add parent directory to python path to allow importing torero_api -parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -sys.path.insert(0, parent_dir) - -# Import main function from the torero_api module -from torero_api.__main__ import main - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/opt/torero-api/scripts/torero_api_ctl.sh b/opt/torero-api/scripts/torero_api_ctl.sh deleted file mode 100755 index c9cb81f..0000000 --- a/opt/torero-api/scripts/torero_api_ctl.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/bash -# -# torero_api_ctl.sh - Control script for torero-api daemon -# -# Usage: torero_api_ctl.sh {start|stop|restart|status|logs} -# - -# Default configuration -DEFAULT_PID_FILE="/tmp/torero-api.pid" -DEFAULT_LOG_FILE="/tmp/torero-api.log" -DEFAULT_HOST="0.0.0.0" -DEFAULT_PORT="8000" - -# Allow overriding via environment variables -PID_FILE="${TORERO_API_PID_FILE:-$DEFAULT_PID_FILE}" -LOG_FILE="${TORERO_API_LOG_FILE:-$DEFAULT_LOG_FILE}" -HOST="${TORERO_API_HOST:-$DEFAULT_HOST}" -PORT="${TORERO_API_PORT:-$DEFAULT_PORT}" - -# Add cool colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Print colored output -print_status() { - local color=$1 - local message=$2 - echo -e "${color}${message}${NC}" -} - -# Get PID from file -get_pid() { - if [ -f "$PID_FILE" ]; then - cat "$PID_FILE" 2>/dev/null - fi -} - -# Check if process is running -is_running() { - local pid=$(get_pid) - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then - echo "$pid" - return 0 - else - return 1 - fi -} - -# Start daemon -start_daemon() { - local pid=$(is_running) - if [ $? -eq 0 ]; then - print_status $YELLOW "torero-api is already running (PID: $pid)" - return 1 - fi - - print_status $BLUE "Starting torero-api daemon..." - print_status $BLUE "Host: $HOST" - print_status $BLUE "Port: $PORT" - print_status $BLUE "PID file: $PID_FILE" - print_status $BLUE "Log file: $LOG_FILE" - - torero-api --daemon \ - --host "$HOST" \ - --port "$PORT" \ - --pid-file "$PID_FILE" \ - --log-file "$LOG_FILE" - - sleep 2 - local pid=$(is_running) - if [ $? -eq 0 ]; then - print_status $GREEN "torero-api started successfully (PID: $pid)" - print_status $GREEN "API available at: http://$HOST:$PORT" - return 0 - else - print_status $RED "Failed to start torero-api" - if [ -f "$LOG_FILE" ]; then - print_status $RED "Check log file: $LOG_FILE" - echo "Last 10 lines of log:" - tail -n 10 "$LOG_FILE" - fi - return 1 - fi -} - -# Stop daemon -stop_daemon() { - local pid=$(is_running) - if [ $? -ne 0 ]; then - print_status $YELLOW "torero-api is not running" - # Clean up stale PID file - [ -f "$PID_FILE" ] && rm -f "$PID_FILE" - return 1 - fi - - print_status $BLUE "Stopping torero-api daemon (PID: $pid)..." - - # Send SIGTERM - kill -TERM "$pid" 2>/dev/null - - # Graceful shutdown - local count=0 - while [ $count -lt 30 ]; do - if ! kill -0 "$pid" 2>/dev/null; then - print_status $GREEN "torero-api stopped successfully" - rm -f "$PID_FILE" - return 0 - fi - sleep 1 - count=$((count + 1)) - done - - # Force kill if still running - print_status $YELLOW "Graceful shutdown failed, forcing termination..." - kill -KILL "$pid" 2>/dev/null - sleep 2 - - if ! kill -0 "$pid" 2>/dev/null; then - print_status $GREEN "torero-api terminated" - rm -f "$PID_FILE" - return 0 - else - print_status $RED "Failed to stop torero-api" - return 1 - fi -} - -# Restart daemon -restart_daemon() { - print_status $BLUE "Restarting torero-api daemon..." - stop_daemon - sleep 2 - start_daemon -} - -# Daemon status -show_status() { - local pid=$(is_running) - if [ $? -eq 0 ]; then - print_status $GREEN "torero-api is running (PID: $pid)" - - # Try to check API health - if command -v curl >/dev/null 2>&1; then - print_status $BLUE "Checking API health..." - local health_response=$(curl -s "http://$HOST:$PORT/health" 2>/dev/null) - if [ $? -eq 0 ]; then - echo "API Health Response:" - echo "$health_response" | python3 -m json.tool 2>/dev/null || echo "$health_response" - else - print_status $YELLOW "API health check failed - service may be starting up" - fi - fi - - return 0 - else - print_status $RED "torero-api is not running" - - # Check for stale PID file - if [ -f "$PID_FILE" ]; then - print_status $YELLOW "Stale PID file found: $PID_FILE" - fi - return 1 - fi -} - -# Show logs -show_logs() { - local lines=${1:-50} - local follow=${2:-false} - - if [ ! -f "$LOG_FILE" ]; then - print_status $RED "Log file not found: $LOG_FILE" - return 1 - fi - - print_status $BLUE "Showing logs from: $LOG_FILE" - echo "----------------------------------------" - - if [ "$follow" = "true" ]; then - tail -f -n "$lines" "$LOG_FILE" - else - tail -n "$lines" "$LOG_FILE" - fi -} - -# Show usage -show_usage() { - echo "Usage: $0 {start|stop|restart|status|logs|follow-logs}" - echo "" - echo "Commands:" - echo " start Start the torero-api daemon" - echo " stop Stop the torero-api daemon" - echo " restart Restart the torero-api daemon" - echo " status Show daemon status" - echo " logs Show recent log entries" - echo " follow-logs Follow log entries (like tail -f)" - echo "" - echo "Environment Variables:" - echo " TORERO_API_HOST API host (default: $DEFAULT_HOST)" - echo " TORERO_API_PORT API port (default: $DEFAULT_PORT)" - echo " TORERO_API_PID_FILE PID file path (default: $DEFAULT_PID_FILE)" - echo " TORERO_API_LOG_FILE Log file path (default: $DEFAULT_LOG_FILE)" - echo "" - echo "Examples:" - echo " $0 start # Start daemon with defaults" - echo " TORERO_API_PORT=8080 $0 start # Start on port 8080" - echo " $0 logs # Show last 50 log lines" - echo " $0 follow-logs # Follow logs in real-time" -} - -# Main logic -case "$1" in - start) - start_daemon - exit $? - ;; - stop) - stop_daemon - exit $? - ;; - restart) - restart_daemon - exit $? - ;; - status) - show_status - exit $? - ;; - logs) - show_logs 50 false - exit $? - ;; - follow-logs) - show_logs 50 true - exit $? - ;; - *) - show_usage - exit 1 - ;; -esac \ No newline at end of file diff --git a/opt/torero-api/torero_api/__init__.py b/opt/torero-api/torero_api/__init__.py deleted file mode 100644 index 96a1279..0000000 --- a/opt/torero-api/torero_api/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -torero-api - RESTful API for torero service management - -This package provides a web API for interacting with torero services, allowing for -discovery and management of services through standard HTTP requests. -""" - -__version__ = "0.1.0" \ No newline at end of file diff --git a/opt/torero-api/torero_api/__main__.py b/opt/torero-api/torero_api/__main__.py deleted file mode 100644 index 75da71f..0000000 --- a/opt/torero-api/torero_api/__main__.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -Command-line interface for torero-api - -This module provides the command-line interface for starting and managing the torero API server. -It can be run directly with "python -m torero_api". -""" - -import argparse -import logging -import sys -import os -import signal -import atexit -from pathlib import Path -from torero_api.server import start_server -from torero_api.core.torero_executor import check_torero_available, check_torero_version - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[ - logging.StreamHandler(sys.stdout), - ] -) -logger = logging.getLogger("torero-api-cli") - -def create_pid_file(pid_file): - """Create PID file with current process ID""" - try: - pid_path = Path(pid_file) - pid_path.parent.mkdir(parents=True, exist_ok=True) - pid_path.write_text(str(os.getpid())) - logger.info(f"PID file created: {pid_file}") - - # Register cleanup function - atexit.register(lambda: cleanup_pid_file(pid_file)) - - except Exception as e: - logger.error(f"Failed to create PID file {pid_file}: {e}") - sys.exit(1) - -def cleanup_pid_file(pid_file): - """Remove PID file on exit""" - try: - pid_path = Path(pid_file) - if pid_path.exists(): - pid_path.unlink() - logger.info(f"PID file removed: {pid_file}") - except Exception as e: - logger.error(f"Failed to remove PID file {pid_file}: {e}") - -def setup_logging_for_daemon(log_file, log_level): - """Setup logging for daemon mode""" - try: - log_path = Path(log_file) - log_path.parent.mkdir(parents=True, exist_ok=True) - - # Remove existing handlers - for handler in logger.handlers[:]: - logger.removeHandler(handler) - - # Create file handler - file_handler = logging.FileHandler(log_file) - file_handler.setLevel(getattr(logging, log_level.upper())) - - # Create formatter - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - file_handler.setFormatter(formatter) - - # Add handler to logger - logger.addHandler(file_handler) - logger.setLevel(getattr(logging, log_level.upper())) - - logger.info(f"Daemon logging initialized: {log_file}") - - except Exception as e: - print(f"Failed to setup daemon logging: {e}") - sys.exit(1) - -def daemonize(pid_file, log_file, log_level): - """ - Daemonize the current process using double fork. - - Args: - pid_file: Path to store the process ID - log_file: Path to redirect logs - log_level: Log level for daemon - """ - try: - # First fork - pid = os.fork() - if pid > 0: - # Parent process - exit - sys.exit(0) - except OSError as e: - logger.error(f"First fork failed: {e}") - sys.exit(1) - - # Decouple from parent environment - os.chdir("/") - os.setsid() - os.umask(0) - - try: - # Second fork - pid = os.fork() - if pid > 0: - # First child - exit - sys.exit(0) - except OSError as e: - logger.error(f"Second fork failed: {e}") - sys.exit(1) - - # Now we're in the daemon process - # Redirect standard file descriptors - sys.stdout.flush() - sys.stderr.flush() - - # Redirect stdin, stdout, stderr to /dev/null or log file - with open('/dev/null', 'r') as f: - os.dup2(f.fileno(), sys.stdin.fileno()) - - # Setup logging before redirecting stdout/stderr - setup_logging_for_daemon(log_file, log_level) - - # Redirect stdout and stderr to log file - with open(log_file, 'a') as f: - os.dup2(f.fileno(), sys.stdout.fileno()) - os.dup2(f.fileno(), sys.stderr.fileno()) - - # Create PID file - create_pid_file(pid_file) - - logger.info("Daemon process started successfully") - -def setup_signal_handlers(): - """Setup signal handlers for graceful shutdown""" - def signal_handler(signum, frame): - signal_name = signal.Signals(signum).name - logger.info(f"Received signal {signal_name} ({signum}), shutting down gracefully...") - sys.exit(0) - - # Register signal handlers - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - - # Ignore SIGHUP in daemon mode - signal.signal(signal.SIGHUP, signal.SIG_IGN) - -def check_daemon_running(pid_file): - """Check if daemon is already running""" - pid_path = Path(pid_file) - - if not pid_path.exists(): - return False, None - - try: - pid = int(pid_path.read_text().strip()) - - # Check if process exists - os.kill(pid, 0) # Send signal 0 to check if process exists - return True, pid - - except (ValueError, ProcessLookupError, PermissionError): - # PID file exists but process doesn't, clean up - try: - pid_path.unlink() - except: - pass - return False, None - -def main(): - """ - Main entry point for the torero API CLI. - - This function handles command-line arguments and starts the API server. - """ - - # Create argument parser - parser = argparse.ArgumentParser( - description="torero API - RESTful API for torero service management", - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - - # Add command-line arguments - parser.add_argument("--host", default="0.0.0.0", help="Host to bind the server to") - parser.add_argument("--port", type=int, default=8000, help="Port to bind the server to") - parser.add_argument("--log-level", default="info", choices=["debug", "info", "warning", "error", "critical"], - help="Log level to use") - parser.add_argument("--reload", action="store_true", help="Enable auto-reload (for development)") - parser.add_argument("--version", action="store_true", help="Show version information and exit") - parser.add_argument("--check", action="store_true", help="Check torero availability and exit") - - # Daemon options - parser.add_argument("--daemon", action="store_true", help="Run as background daemon") - parser.add_argument("--pid-file", default="/tmp/torero-api.pid", help="PID file for daemon mode") - parser.add_argument("--log-file", default="/tmp/torero-api.log", help="Log file for daemon mode") - - # Parse arguments - args = parser.parse_args() - - # Show version information if requested - if args.version: - from torero_api import __version__ - print(f"torero-api version: {__version__}") - - # Check torero version - available, message = check_torero_available() - if available: - torero_version = check_torero_version() - print(f"torero version: {torero_version}") - else: - print(f"torero: {message}") - - sys.exit(0) - - # Check torero availability if requested - if args.check: - available, message = check_torero_available() - if available: - print(f"torero: Available ({check_torero_version()})") - sys.exit(0) - else: - print(f"torero: Not available - {message}") - sys.exit(1) - - # Daemon mode validation - if args.daemon: - if args.reload: - logger.error("Cannot use --reload with --daemon mode") - sys.exit(1) - - # Check if daemon is already running - is_running, existing_pid = check_daemon_running(args.pid_file) - if is_running: - logger.error(f"torero-api daemon is already running (PID: {existing_pid})") - sys.exit(1) - - logger.info(f"Starting torero-api in daemon mode...") - logger.info(f"PID file: {args.pid_file}") - logger.info(f"Log file: {args.log_file}") - logger.info(f"API will be available at: http://{args.host}:{args.port}") - - # Daemonize the process - daemonize(args.pid_file, args.log_file, args.log_level) - - # Setup signal handlers for graceful shutdown - setup_signal_handlers() - - logger.info(f"Daemon started successfully (PID: {os.getpid()})") - - # Check if torero is available before starting the server - available, message = check_torero_available() - if not available: - logger.warning(f"torero not available: {message}") - logger.warning("The API will start, but some functionality may not work correctly.") - - # Start the server - try: - logger.info(f"Starting torero API server on {args.host}:{args.port}") - start_server( - host=args.host, - port=args.port, - log_level=args.log_level, - reload=args.reload - ) - except KeyboardInterrupt: - logger.info("Server stopped by user") - except Exception as e: - logger.error(f"Error starting server: {str(e)}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/__init__.py b/opt/torero-api/torero_api/api/__init__.py deleted file mode 100644 index c141683..0000000 --- a/opt/torero-api/torero_api/api/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Version 1 of the torero API - -This package contains the v1 implementation of the torero API endpoints. -It follows RESTful principles and is organized by resource type. - -This version provides endpoints for: -- Service discovery and metadata -- Service filtering by type and tags -- Service type and tag enumeration -- Decorator discovery and metadata -- Repository discovery and metadata -- Secret discovery and metadata -- Service execution - -All endpoints are designed to be MCP-compatible, providing consistent -response formats and comprehensive type information. -""" - -# Import and expose the endpoints for easier access -from torero_api.api.v1.endpoints import services, decorators, repositories, secrets, execution \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/__init__.py b/opt/torero-api/torero_api/api/v1/__init__.py deleted file mode 100644 index d218df3..0000000 --- a/opt/torero-api/torero_api/api/v1/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Version 1 of the torero API - -This package contains the v1 implementation of the torero API endpoints. -It follows RESTful principles and is organized by resource type. - -This version provides endpoints for: -- Service discovery and metadata -- Service filtering by type and tags -- Service type and tag enumeration - -All endpoints are designed to be MCP-compatible, providing consistent -response formats and comprehensive type information. -""" - -# Import and expose the endpoints for easier access -from torero_api.api.v1.endpoints import services \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/__init__.py b/opt/torero-api/torero_api/api/v1/endpoints/__init__.py deleted file mode 100644 index 182a71a..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Endpoint modules for the v1 torero API - -This package contains endpoint modules organized by resource type. -Each module implements a FastAPI router with endpoints for a specific -resource category. - -Current endpoints: -- services: Endpoints for discovering and filtering torero services -- decorators: Endpoints for discovering and filtering torero decorators -- repositories: Endpoints for discovering and filtering torero repositories -- secrets: Endpoints for discovering and filtering torero secrets -- execution: Endpoints for executing torero services - -All endpoints follow RESTful principles and provide comprehensive -OpenAPI documentation for MCP compatibility. -""" \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/database.py b/opt/torero-api/torero_api/api/v1/endpoints/database.py deleted file mode 100644 index 52efc90..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/database.py +++ /dev/null @@ -1,251 +0,0 @@ -from typing import Optional, Dict, Any -from fastapi import APIRouter, HTTPException, File, UploadFile, Form -from fastapi.responses import Response -import tempfile -import os - -from torero_api.core.torero_executor import ( - execute_db_export, - execute_db_import, - ToreroError -) -from torero_api.models.database import ( - DatabaseExportFormat, - DatabaseImportOptions, - DatabaseImportCheckResult -) - -router = APIRouter( - prefix="/db", - tags=["database"], - responses={404: {"description": "Not found"}}, -) - -@router.get("/export") -async def export_database( - format: DatabaseExportFormat = DatabaseExportFormat.YAML -) -> Dict[str, Any]: - """ - Export services and resources to a file. - - This endpoint exports all torero configurations including decorators, - repositories, and services in the specified format. - - Args: - format: The output format (json or yaml). Defaults to yaml. - - Returns: - The exported configuration data. - - Raises: - HTTPException: If the export operation fails. - """ - try: - result = await execute_db_export(format=format.value) - # If YAML format, the result contains the raw YAML data - if format == DatabaseExportFormat.YAML and isinstance(result, dict) and "data" in result: - # For API response, we need to parse YAML to JSON - import yaml - try: - data = yaml.safe_load(result["data"]) - return data - except yaml.YAMLError: - # If YAML parsing fails, return the raw data - return {"data": result["data"], "format": "yaml"} - return result - except ToreroError as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/export/download") -async def download_database_export( - format: DatabaseExportFormat = DatabaseExportFormat.YAML, - filename: Optional[str] = None -) -> Response: - """ - Export services and resources as a downloadable file. - - This endpoint exports all torero configurations and returns them as a - downloadable file with appropriate content type headers. - - Args: - format: The output format (json or yaml). Defaults to yaml. - filename: Optional custom filename for the export. - - Returns: - A file response with the exported data. - - Raises: - HTTPException: If the export operation fails. - """ - try: - result = await execute_db_export(format=format.value) - - # Determine content type and default filename based on format - if format == DatabaseExportFormat.JSON: - content_type = "application/json" - default_filename = "torero-export.json" - # Convert dict to JSON string if needed - if isinstance(result, dict): - import json - content = json.dumps(result, indent=2) - else: - content = result - else: - content_type = "application/x-yaml" - default_filename = "torero-export.yaml" - # Get the raw YAML data if it's wrapped - if isinstance(result, dict) and "data" in result: - content = result["data"] - else: - # Convert dict to YAML if needed - import yaml - content = yaml.dump(result, default_flow_style=False) - - filename = filename or default_filename - - return Response( - content=content if isinstance(content, (str, bytes)) else str(content), - media_type=content_type, - headers={ - "Content-Disposition": f"attachment; filename={filename}" - } - ) - except ToreroError as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.post("/import") -async def import_database( - file: UploadFile = File(...), - force: bool = Form(False), - check: bool = Form(False), - validate_only: bool = Form(False) -) -> Dict[str, Any]: - """ - Import resources/services from a service file. - - This endpoint imports services and resources from an uploaded file to move - configurations between torero instances. - - Args: - file: The service configuration file to import. - force: Override existing services. - check: Perform validation and dry-run of import. - validate_only: Validate service file only. - - Returns: - Import result including status and any conflicts or changes. - - Raises: - HTTPException: If the import operation fails. - """ - try: - # Save uploaded file to temporary location - with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as tmp_file: - content = await file.read() - tmp_file.write(content) - tmp_file_path = tmp_file.name - - try: - # Execute import with options - options = DatabaseImportOptions( - force=force, - check=check, - validate_only=validate_only - ) - result = await execute_db_import(file_path=tmp_file_path, options=options) - return result - finally: - # Clean up temporary file - os.unlink(tmp_file_path) - - except ToreroError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Import failed: {str(e)}") - -@router.post("/import/check") -async def check_import( - file: UploadFile = File(...) -) -> DatabaseImportCheckResult: - """ - Check what would happen during an import without actually importing. - - This endpoint performs a dry-run of the import operation to show what - resources would be added, replaced, or conflict. - - Args: - file: The service configuration file to check. - - Returns: - Check results showing conflicts, additions, and potential replacements. - - Raises: - HTTPException: If the check operation fails. - """ - try: - # Save uploaded file to temporary location - with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as tmp_file: - content = await file.read() - tmp_file.write(content) - tmp_file_path = tmp_file.name - - try: - # Execute import check - options = DatabaseImportOptions(check=True) - result = await execute_db_import(file_path=tmp_file_path, options=options) - return DatabaseImportCheckResult(**result) - finally: - # Clean up temporary file - os.unlink(tmp_file_path) - - except ToreroError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Check failed: {str(e)}") - -@router.post("/import/repository") -async def import_from_repository( - repository: str = Form(...), - file_path: str = Form(...), - reference: Optional[str] = Form(None), - private_key_name: Optional[str] = Form(None), - force: bool = Form(False), - check: bool = Form(False), - validate_only: bool = Form(False) -) -> Dict[str, Any]: - """ - Import resources/services from a repository. - - This endpoint imports services and resources from a file in a git repository. - Supports both HTTP and SSH repositories with optional private key authentication. - - Args: - repository: Repository URL (HTTP or SSH). - file_path: Path to the import file within the repository. - reference: Optional branch/tag/commit reference. - private_key_name: Optional private key name for SSH authentication. - force: Override existing services. - check: Perform validation and dry-run of import. - validate_only: Validate service file only. - - Returns: - Import result including status and any conflicts or changes. - - Raises: - HTTPException: If the import operation fails. - """ - try: - options = DatabaseImportOptions( - repository=repository, - reference=reference, - private_key=private_key_name, - force=force, - check=check, - validate_only=validate_only - ) - result = await execute_db_import(file_path=file_path, options=options) - return result - except ToreroError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Repository import failed: {str(e)}") \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/decorators.py b/opt/torero-api/torero_api/api/v1/endpoints/decorators.py deleted file mode 100644 index b3571fd..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/decorators.py +++ /dev/null @@ -1,253 +0,0 @@ -""" -Decorators endpoints for torero API - -This module defines the API endpoints for interacting with torero decorators. -""" - -from fastapi import APIRouter, HTTPException, Query, Path, Depends -from typing import Optional, List -import logging - -from torero_api.models.decorator import Decorator -from torero_api.core.torero_executor import get_decorators, get_decorator_by_name, describe_decorator - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -# Adding pagination parameters -def common_parameters( - skip: int = Query(0, ge=0, description="Number of items to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of items to return") -): - """ - Common pagination parameters for endpoints that return lists. - - Args: - skip: Number of items to skip - limit: Maximum number of items to return - - Returns: - dict: Dictionary containing the pagination parameters - """ - return {"skip": skip, "limit": limit} - -@router.get( - "/", - response_model=List[Decorator], - summary="List decorators", - description=""" - Get all registered torero decorators with optional filtering. - - This endpoint returns a list of all decorators registered with torero. - You can filter the results by decorator type to narrow down the list. - - Examples: - - List all decorators: GET /v1/decorators/ - - List all authentication decorators: GET /v1/decorators/?type=authentication - """ -) -def list_decorators( - commons: dict = Depends(common_parameters), - type: Optional[str] = Query( - None, - description="Filter by decorator type, e.g. 'authentication'" - ) -): - """ - List all torero decorators, optionally filtered by type. - - Args: - commons: Common pagination parameters - type: Optional filter to return only decorators of a specific type - - Returns: - List[Decorator]: List of Decorator objects matching the filter criteria - - Raises: - HTTPException: If an error occurs while retrieving or filtering decorators - """ - try: - logger.info(f"Getting decorators with filter - type: {type}") - decorators = get_decorators() - - # Apply filters if provided - if type: - decorators = [d for d in decorators if d.type == type] - - # Apply pagination - skip = commons["skip"] - limit = commons["limit"] - paginated_decorators = decorators[skip:skip + limit] - - logger.info(f"Returning {len(paginated_decorators)} decorators after filtering") - return paginated_decorators - - except Exception as e: - logger.error(f"Error in list_decorators: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/types", - response_model=List[str], - summary="List decorator types", - description=""" - Return a list of unique decorator types used by registered decorators. - - This endpoint provides a list of all distinct decorator types that are - currently in use across all registered decorators. - - This information is useful for: - - Building UI dropdown filters - - Understanding what types of decorators are available - - Validating type values for new decorators - """ -) -def list_decorator_types(): - """ - Return a list of unique decorator types used by registered decorators. - - Retrieves all decorators and extracts the unique set of decorator types, - returning them as a sorted list. - - Returns: - List[str]: Sorted list of unique decorator type strings - - Raises: - HTTPException: If an error occurs while retrieving decorators - """ - try: - logger.info("Getting decorator types") - decorators = get_decorators() - types = sorted(set(d.type for d in decorators)) - logger.info(f"Returning {len(types)} decorator types") - return types - except Exception as e: - logger.error(f"Error in list_decorator_types: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}", - response_model=Decorator, - summary="Get decorator details", - description=""" - Get detailed information about a specific decorator by name. - - This endpoint retrieves detailed information about a single decorator - identified by its name. The name is case-sensitive and must match - exactly the name of a registered decorator. - - If no decorator is found with the specified name, a 404 error is returned. - """ -) -def get_decorator( - name: str = Path( - ..., - description="Name of the decorator to retrieve" - ) -): - """ - Get detailed information about a specific decorator by name. - - Args: - name: The name of the decorator to retrieve - - Returns: - Decorator: The decorator with the specified name - - Raises: - HTTPException: If the decorator is not found or an error occurs - """ - try: - logger.info(f"Getting decorator details for: {name}") - decorator = get_decorator_by_name(name) - - if decorator: - logger.info(f"Found decorator: {name}") - return decorator - - # If we get here, the decorator was not found - logger.warning(f"Decorator not found: {name}") - raise HTTPException(status_code=404, detail=f"Decorator '{name}' not found") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error retrieving decorator: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}/describe", - response_model=dict, - summary="Get detailed decorator description", - description=""" - Get detailed description of a specific decorator by name. - - This endpoint returns comprehensive information about a decorator, - including all configuration details, parameters, and metadata. - - The name is case-sensitive and must match exactly the name of a registered decorator. - - If no decorator is found with the specified name, a 404 error is returned. - """ -) -def describe_decorator_detail( - name: str = Path( - ..., - description="Name of the decorator to describe", - examples={ - "audit-decorator": { - "summary": "Audit Decorator", - "description": "Get detailed description of an audit decorator", - "value": "audit-decorator" - }, - "logging-decorator": { - "summary": "Logging Decorator", - "description": "Get detailed description of a logging decorator", - "value": "logging-decorator" - } - } - ) -): - """ - Get detailed description of a specific decorator. - - Args: - name: The name of the decorator to describe - - Returns: - dict: Detailed decorator information - - Raises: - HTTPException: If the decorator is not found or if there's an error - """ - try: - logger.info(f"Getting detailed description for decorator: {name}") - - # First verify the decorator exists - decorator = get_decorator_by_name(name) - if not decorator: - logger.warning(f"Decorator not found: {name}") - raise HTTPException(status_code=404, detail=f"Decorator '{name}' not found") - - # Get detailed description - description = describe_decorator(name) - - if description is None: - logger.warning(f"Could not retrieve detailed description for decorator: {name}") - raise HTTPException(status_code=404, detail=f"Could not retrieve detailed description for decorator '{name}'") - - return description - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error getting detailed decorator description: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - except Exception as e: - logger.error(f"Error in get_decorator: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/execution.py b/opt/torero-api/torero_api/api/v1/endpoints/execution.py deleted file mode 100644 index 30cb11b..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/execution.py +++ /dev/null @@ -1,330 +0,0 @@ -""" -Execution endpoints for torero API - -This module defines the API endpoints for executing torero services. -""" - -from fastapi import APIRouter, HTTPException, Path, Query -from typing import Dict, Any, Optional -import logging - -from torero_api.models.execution import ServiceExecutionResult -from torero_api.core.torero_executor import ( - run_ansible_playbook_service, - run_python_script_service, - run_opentofu_plan_apply_service, - run_opentofu_plan_destroy_service, - get_service_by_name -) - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -@router.post( - "/ansible-playbook/{name}", - response_model=ServiceExecutionResult, - summary="Run Ansible playbook service", - description=""" - Execute a registered torero Ansible playbook service by name. - - This endpoint runs the specified Ansible playbook service and returns - the execution results, including standard output, standard error, - return code, and timing information. - - The name is case-sensitive and must match exactly the name of a registered - Ansible playbook service. - - If no service is found with the specified name, a 404 error is returned. - """ -) -def run_ansible_service( - name: str = Path( - ..., - description="Name of the Ansible playbook service to run", - examples={ - "network-backup": { - "summary": "Network Backup Service", - "description": "Run a network backup service", - "value": "network-backup" - }, - "hello-ansible": { - "summary": "Hello World Ansible Service", - "description": "Run a simple hello world Ansible service", - "value": "hello-ansible" - } - } - ) -): - """ - Execute a registered torero Ansible playbook service. - - Args: - name: The name of the Ansible playbook service to run - - Returns: - ServiceExecutionResult: The results of the service execution - - Raises: - HTTPException: If the service is not found, is not an Ansible playbook, or execution fails - """ - try: - logger.info(f"Running Ansible playbook service: {name}") - - # First, verify the service exists and is an Ansible playbook - service = get_service_by_name(name) - if not service: - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - if service.type != "ansible-playbook": - logger.warning(f"Service {name} is not an Ansible playbook (type: {service.type})") - raise HTTPException( - status_code=400, - detail=f"Service '{name}' is not an Ansible playbook (type: {service.type})" - ) - - # Execute the service - result = run_ansible_playbook_service(name) - - # Convert the result to the response model - return ServiceExecutionResult(**result) - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error running Ansible playbook service: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.post( - "/python-script/{name}", - response_model=ServiceExecutionResult, - summary="Run Python script service", - description=""" - Execute a registered torero Python script service by name. - - This endpoint runs the specified Python script service and returns - the execution results, including standard output, standard error, - return code, and timing information. - - The name is case-sensitive and must match exactly the name of a registered - Python script service. - - If no service is found with the specified name, a 404 error is returned. - """ -) -def run_python_script( - name: str = Path( - ..., - description="Name of the Python script service to run", - examples={ - "hello-python": { - "summary": "Hello World Python Script", - "description": "Run a simple hello world Python script", - "value": "hello-python" - }, - "data-processor": { - "summary": "Data Processing Script", - "description": "Run a Python script that processes data", - "value": "data-processor" - } - } - ) -): - """ - Execute a registered torero Python script service. - - Args: - name: The name of the Python script service to run - - Returns: - ServiceExecutionResult: The results of the service execution - - Raises: - HTTPException: If the service is not found, is not a Python script, or execution fails - """ - try: - logger.info(f"Running Python script service: {name}") - - # First, verify the service exists and is a Python script - service = get_service_by_name(name) - if not service: - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - if service.type != "python-script": - logger.warning(f"Service {name} is not a Python script (type: {service.type})") - raise HTTPException( - status_code=400, - detail=f"Service '{name}' is not a Python script (type: {service.type})" - ) - - # Execute the service - result = run_python_script_service(name) - - # Convert the result to the response model - return ServiceExecutionResult(**result) - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error running Python script service: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.post( - "/opentofu-plan/{name}/apply", - response_model=ServiceExecutionResult, - summary="Apply OpenTofu plan service", - description=""" - Execute a registered torero OpenTofu plan service to apply infrastructure changes. - - This endpoint runs the specified OpenTofu plan service in apply mode and returns - the execution results, including standard output, standard error, - return code, and timing information. - - The name is case-sensitive and must match exactly the name of a registered - OpenTofu plan service. - - If no service is found with the specified name, a 404 error is returned. - """ -) -def apply_opentofu_plan( - name: str = Path( - ..., - description="Name of the OpenTofu plan service to apply", - examples={ - "infrastructure-deploy": { - "summary": "Infrastructure Deployment", - "description": "Apply infrastructure changes using OpenTofu", - "value": "infrastructure-deploy" - }, - "cloud-resources": { - "summary": "Cloud Resources", - "description": "Apply cloud resource configuration", - "value": "cloud-resources" - } - } - ) -): - """ - Apply a registered torero OpenTofu plan service. - - Args: - name: The name of the OpenTofu plan service to apply - - Returns: - ServiceExecutionResult: The results of the service execution - - Raises: - HTTPException: If the service is not found, is not an OpenTofu plan, or execution fails - """ - try: - logger.info(f"Applying OpenTofu plan service: {name}") - - # First, verify the service exists and is an OpenTofu plan - service = get_service_by_name(name) - if not service: - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - if service.type != "opentofu-plan": - logger.warning(f"Service {name} is not an OpenTofu plan (type: {service.type})") - raise HTTPException( - status_code=400, - detail=f"Service '{name}' is not an OpenTofu plan (type: {service.type})" - ) - - # Execute the service - result = run_opentofu_plan_apply_service(name) - - # Convert the result to the response model - return ServiceExecutionResult(**result) - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error applying OpenTofu plan service: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.post( - "/opentofu-plan/{name}/destroy", - response_model=ServiceExecutionResult, - summary="Destroy OpenTofu plan service resources", - description=""" - Execute a registered torero OpenTofu plan service to destroy infrastructure resources. - - This endpoint runs the specified OpenTofu plan service in destroy mode and returns - the execution results, including standard output, standard error, - return code, and timing information. - - The name is case-sensitive and must match exactly the name of a registered - OpenTofu plan service. - - If no service is found with the specified name, a 404 error is returned. - - WARNING: This operation will destroy infrastructure resources and cannot be undone. - """ -) -def destroy_opentofu_plan( - name: str = Path( - ..., - description="Name of the OpenTofu plan service to destroy", - examples={ - "infrastructure-deploy": { - "summary": "Infrastructure Deployment", - "description": "Destroy infrastructure managed by OpenTofu", - "value": "infrastructure-deploy" - }, - "cloud-resources": { - "summary": "Cloud Resources", - "description": "Destroy cloud resource configuration", - "value": "cloud-resources" - } - } - ) -): - """ - Destroy resources managed by a registered torero OpenTofu plan service. - - Args: - name: The name of the OpenTofu plan service to destroy - - Returns: - ServiceExecutionResult: The results of the service execution - - Raises: - HTTPException: If the service is not found, is not an OpenTofu plan, or execution fails - """ - try: - logger.info(f"Destroying OpenTofu plan service: {name}") - - # First, verify the service exists and is an OpenTofu plan - service = get_service_by_name(name) - if not service: - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - if service.type != "opentofu-plan": - logger.warning(f"Service {name} is not an OpenTofu plan (type: {service.type})") - raise HTTPException( - status_code=400, - detail=f"Service '{name}' is not an OpenTofu plan (type: {service.type})" - ) - - # Execute the service - result = run_opentofu_plan_destroy_service(name) - - # Convert the result to the response model - return ServiceExecutionResult(**result) - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error destroying OpenTofu plan service: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/registries.py b/opt/torero-api/torero_api/api/v1/endpoints/registries.py deleted file mode 100644 index abf775b..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/registries.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Registry endpoints for torero API - -This module defines the API endpoints for managing torero registries. -""" - -from fastapi import APIRouter, HTTPException, Query, Path -from typing import Optional, List -import logging - -from torero_api.models.registry import Registry -from torero_api.core.torero_executor import get_registries, get_registry_by_name - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -@router.get( - "/", - response_model=List[Registry], - summary="List all registries", - description=""" - Get a list of all registered torero registries. - - This endpoint returns all registries configured in torero, including their - type, URL, and metadata. You can optionally filter by registry type. - - Registries are locations where packages, modules, or artifacts can be stored - and retrieved, such as Ansible Galaxy or PyPI. - """ -) -def list_registries( - type: Optional[str] = Query( - None, - description="Filter registries by type (e.g., 'ansible-galaxy', 'pypi')", - examples={ - "ansible-galaxy": { - "summary": "Filter for Ansible Galaxy registries", - "description": "Show only Ansible Galaxy registries", - "value": "ansible-galaxy" - }, - "pypi": { - "summary": "Filter for PyPI registries", - "description": "Show only PyPI registries", - "value": "pypi" - } - } - ) -): - """ - List all registries, optionally filtered by type. - - Args: - type: Optional type filter - - Returns: - List[Registry]: List of registries matching the criteria - - Raises: - HTTPException: If there's an error retrieving registries - """ - try: - logger.info(f"Listing registries with type filter: {type}") - - # Get all registries from torero - registries = get_registries() - - # Apply type filter if provided - if type: - registries = [reg for reg in registries if reg.type == type] - logger.info(f"Filtered to {len(registries)} registries of type '{type}'") - else: - logger.info(f"Retrieved {len(registries)} registries") - - return registries - - except Exception as e: - logger.error(f"Error retrieving registries: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/types", - response_model=List[str], - summary="List unique registry types", - description=""" - Get a list of all unique registry types. - - This endpoint returns a deduplicated list of all registry types that are - currently registered in torero, such as 'ansible-galaxy', 'pypi', etc. - """ -) -def list_registry_types(): - """ - List all unique registry types. - - Returns: - List[str]: Sorted list of unique registry types - - Raises: - HTTPException: If there's an error retrieving registries - """ - try: - logger.info("Getting unique registry types") - - # Get all registries - registries = get_registries() - - # Extract unique types - types = sorted(list(set(reg.type for reg in registries))) - - logger.info(f"Found {len(types)} unique registry types") - return types - - except Exception as e: - logger.error(f"Error retrieving registry types: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}", - response_model=Registry, - summary="Get registry by name", - description=""" - Get a specific registry by its name. - - This endpoint returns detailed information about a single registry, - including its type, URL, and metadata. - - The name is case-sensitive and must match exactly the name of a registered registry. - - If no registry is found with the specified name, a 404 error is returned. - """ -) -def get_registry( - name: str = Path( - ..., - description="Name of the registry to retrieve", - examples={ - "ansible-galaxy-main": { - "summary": "Ansible Galaxy Registry", - "description": "Get the main Ansible Galaxy registry", - "value": "ansible-galaxy-main" - }, - "pypi-internal": { - "summary": "Internal PyPI Registry", - "description": "Get an internal PyPI registry", - "value": "pypi-internal" - } - } - ) -): - """ - Get a specific registry by name. - - Args: - name: The name of the registry to retrieve - - Returns: - Registry: The requested registry - - Raises: - HTTPException: If the registry is not found or if there's an error - """ - try: - logger.info(f"Retrieving registry: {name}") - - # Get the specific registry - registry = get_registry_by_name(name) - - if registry: - logger.info(f"Found registry: {name}") - return registry - - # If we get here, the registry was not found - logger.warning(f"Registry not found: {name}") - raise HTTPException(status_code=404, detail=f"Registry '{name}' not found") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error retrieving registry: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/repositories.py b/opt/torero-api/torero_api/api/v1/endpoints/repositories.py deleted file mode 100644 index 994265d..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/repositories.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -Repositories endpoints for torero API - -This module defines the API endpoints for interacting with torero repositories. -""" - -from fastapi import APIRouter, HTTPException, Query, Path, Depends -from typing import Optional, List -import logging - -from torero_api.models.repository import Repository -from torero_api.core.torero_executor import get_repositories, get_repository_by_name, describe_repository - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -# Adding pagination parameters -def common_parameters( - skip: int = Query(0, ge=0, description="Number of items to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of items to return") -): - """ - Common pagination parameters for endpoints that return lists. - - Args: - skip: Number of items to skip - limit: Maximum number of items to return - - Returns: - dict: Dictionary containing the pagination parameters - """ - return {"skip": skip, "limit": limit} - -@router.get( - "/", - response_model=List[Repository], - summary="List repositories", - description=""" - Get all registered torero repositories with optional filtering. - - This endpoint returns a list of all repositories registered with torero. - You can filter the results by repository type to narrow down the list. - - Examples: - - List all repositories: GET /v1/repositories/ - - List all file repositories: GET /v1/repositories/?type=file - - List all git repositories: GET /v1/repositories/?type=git - """ -) -def list_repositories( - commons: dict = Depends(common_parameters), - type: Optional[str] = Query( - None, - description="Filter by repository type, e.g. 'file', 'git', 's3'" - ) -): - """ - List all torero repositories, optionally filtered by type. - - Args: - commons: Common pagination parameters - type: Optional filter to return only repositories of a specific type - - Returns: - List[Repository]: List of Repository objects matching the filter criteria - - Raises: - HTTPException: If an error occurs while retrieving or filtering repositories - """ - try: - logger.info(f"Getting repositories with filter - type: {type}") - repositories = get_repositories() - - # Apply filters if provided - if type: - repositories = [r for r in repositories if r.type == type] - - # Apply pagination - skip = commons["skip"] - limit = commons["limit"] - paginated_repositories = repositories[skip:skip + limit] - - logger.info(f"Returning {len(paginated_repositories)} repositories after filtering") - return paginated_repositories - - except Exception as e: - logger.error(f"Error in list_repositories: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/types", - response_model=List[str], - summary="List repository types", - description=""" - Return a list of unique repository types used by registered repositories. - - This endpoint provides a list of all distinct repository types (e.g., file, git, s3) - that are currently in use across all registered repositories. - - This information is useful for: - - Building UI dropdown filters - - Understanding what types of repositories are available - - Validating type values for new repositories - """ -) -def list_repository_types(): - """ - Return a list of unique repository types used by registered repositories. - - Retrieves all repositories and extracts the unique set of repository types, - returning them as a sorted list. - - Returns: - List[str]: Sorted list of unique repository type strings - - Raises: - HTTPException: If an error occurs while retrieving repositories - """ - try: - logger.info("Getting repository types") - repositories = get_repositories() - types = sorted(set(r.type for r in repositories)) - logger.info(f"Returning {len(types)} repository types") - return types - except Exception as e: - logger.error(f"Error in list_repository_types: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}", - response_model=Repository, - summary="Get repository details", - description=""" - Get detailed information about a specific repository by name. - - This endpoint retrieves detailed information about a single repository - identified by its name. The name is case-sensitive and must match - exactly the name of a registered repository. - - If no repository is found with the specified name, a 404 error is returned. - """ -) -def get_repository( - name: str = Path( - ..., - description="Name of the repository to retrieve" - ) -): - """ - Get detailed information about a specific repository by name. - - Args: - name: The name of the repository to retrieve - - Returns: - Repository: The repository with the specified name - - Raises: - HTTPException: If the repository is not found or an error occurs - """ - try: - logger.info(f"Getting repository details for: {name}") - repository = get_repository_by_name(name) - - if repository: - logger.info(f"Found repository: {name}") - return repository - - # If we get here, the repository was not found - logger.warning(f"Repository not found: {name}") - raise HTTPException(status_code=404, detail=f"Repository '{name}' not found") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error retrieving repository: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}/describe", - response_model=dict, - summary="Get detailed repository description", - description=""" - Get detailed description of a specific repository by name. - - This endpoint returns comprehensive information about a repository, - including all configuration details, metadata, and additional properties - that may not be included in the standard repository object. - - The name is case-sensitive and must match exactly the name of a registered repository. - - If no repository is found with the specified name, a 404 error is returned. - """ -) -def describe_repository_detail( - name: str = Path( - ..., - description="Name of the repository to describe", - examples={ - "ansible-roles": { - "summary": "Ansible Roles Repository", - "description": "Get detailed description of an Ansible roles repository", - "value": "ansible-roles" - }, - "terraform-modules": { - "summary": "Terraform Modules Repository", - "description": "Get detailed description of a Terraform modules repository", - "value": "terraform-modules" - } - } - ) -): - """ - Get detailed description of a specific repository. - - Args: - name: The name of the repository to describe - - Returns: - dict: Detailed repository information - - Raises: - HTTPException: If the repository is not found or if there's an error - """ - try: - logger.info(f"Getting detailed description for repository: {name}") - - # First verify the repository exists - repository = get_repository_by_name(name) - if not repository: - logger.warning(f"Repository not found: {name}") - raise HTTPException(status_code=404, detail=f"Repository '{name}' not found") - - # Get detailed description - description = describe_repository(name) - - if description is None: - logger.warning(f"Could not retrieve detailed description for repository: {name}") - raise HTTPException(status_code=404, detail=f"Could not retrieve detailed description for repository '{name}'") - - return description - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error getting detailed repository description: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - except Exception as e: - logger.error(f"Error in get_repository: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/secrets.py b/opt/torero-api/torero_api/api/v1/endpoints/secrets.py deleted file mode 100644 index 0d0b862..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/secrets.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -Secrets endpoints for torero API - -This module defines the API endpoints for interacting with torero secrets. -""" - -from fastapi import APIRouter, HTTPException, Query, Path, Depends -from typing import Optional, List -import logging - -from torero_api.models.secret import Secret -from torero_api.core.torero_executor import get_secrets, get_secret_by_name, describe_secret - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -# Adding pagination parameters -def common_parameters( - skip: int = Query(0, ge=0, description="Number of items to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of items to return") -): - """ - Common pagination parameters for endpoints that return lists. - - Args: - skip: Number of items to skip - limit: Maximum number of items to return - - Returns: - dict: Dictionary containing the pagination parameters - """ - return {"skip": skip, "limit": limit} - -@router.get( - "/", - response_model=List[Secret], - summary="List secrets", - description=""" - Get all registered torero secrets with optional filtering. - - This endpoint returns a list of all secrets registered with torero. - You can filter the results by secret type to narrow down the list. - - Examples: - - List all secrets: GET /v1/secrets/ - - List all password secrets: GET /v1/secrets/?type=password - - List all API key secrets: GET /v1/secrets/?type=api-key - """ -) -def list_secrets( - commons: dict = Depends(common_parameters), - type: Optional[str] = Query( - None, - description="Filter by secret type, e.g. 'password', 'api-key', 'token'" - ) -): - """ - List all torero secrets, optionally filtered by type. - - Args: - commons: Common pagination parameters - type: Optional filter to return only secrets of a specific type - - Returns: - List[Secret]: List of Secret objects matching the filter criteria - - Raises: - HTTPException: If an error occurs while retrieving or filtering secrets - """ - try: - logger.info(f"Getting secrets with filter - type: {type}") - secrets = get_secrets() - - # Apply filters if provided - if type: - secrets = [s for s in secrets if s.type == type] - - # Apply pagination - skip = commons["skip"] - limit = commons["limit"] - paginated_secrets = secrets[skip:skip + limit] - - logger.info(f"Returning {len(paginated_secrets)} secrets after filtering") - return paginated_secrets - - except Exception as e: - logger.error(f"Error in list_secrets: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/types", - response_model=List[str], - summary="List secret types", - description=""" - Return a list of unique secret types used by registered secrets. - - This endpoint provides a list of all distinct secret types (e.g., password, api-key, token) - that are currently in use across all registered secrets. - - This information is useful for: - - Building UI dropdown filters - - Understanding what types of secrets are available - - Validating type values for new secrets - """ -) -def list_secret_types(): - """ - Return a list of unique secret types used by registered secrets. - - Retrieves all secrets and extracts the unique set of secret types, - returning them as a sorted list. - - Returns: - List[str]: Sorted list of unique secret type strings - - Raises: - HTTPException: If an error occurs while retrieving secrets - """ - try: - logger.info("Getting secret types") - secrets = get_secrets() - types = sorted(set(s.type for s in secrets)) - logger.info(f"Returning {len(types)} secret types") - return types - except Exception as e: - logger.error(f"Error in list_secret_types: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}", - response_model=Secret, - summary="Get secret details", - description=""" - Get detailed information about a specific secret by name. - - This endpoint retrieves metadata about a single secret identified by its name. - The name is case-sensitive and must match exactly the name of a registered secret. - - Note: For security reasons, this endpoint only returns metadata about the secret, - not the actual secret value. - - If no secret is found with the specified name, a 404 error is returned. - """ -) -def get_secret( - name: str = Path( - ..., - description="Name of the secret to retrieve" - ) -): - """ - Get detailed information about a specific secret by name. - - Args: - name: The name of the secret to retrieve - - Returns: - Secret: The secret with the specified name - - Raises: - HTTPException: If the secret is not found or an error occurs - """ - try: - logger.info(f"Getting secret details for: {name}") - secret = get_secret_by_name(name) - - if secret: - logger.info(f"Found secret: {name}") - return secret - - # If we get here, the secret was not found - logger.warning(f"Secret not found: {name}") - raise HTTPException(status_code=404, detail=f"Secret '{name}' not found") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error retrieving secret: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}/describe", - response_model=dict, - summary="Get detailed secret description", - description=""" - Get detailed description of a specific secret by name. - - This endpoint returns comprehensive information about a secret, - including all configuration details and metadata. Note that the actual - secret value is not returned for security reasons. - - The name is case-sensitive and must match exactly the name of a registered secret. - - If no secret is found with the specified name, a 404 error is returned. - """ -) -def describe_secret_detail( - name: str = Path( - ..., - description="Name of the secret to describe", - examples={ - "ssh-key": { - "summary": "SSH Key", - "description": "Get detailed description of an SSH key secret", - "value": "ssh-key" - }, - "api-token": { - "summary": "API Token", - "description": "Get detailed description of an API token secret", - "value": "api-token" - } - } - ) -): - """ - Get detailed description of a specific secret. - - Args: - name: The name of the secret to describe - - Returns: - dict: Detailed secret information (without the actual secret value) - - Raises: - HTTPException: If the secret is not found or if there's an error - """ - try: - logger.info(f"Getting detailed description for secret: {name}") - - # First verify the secret exists - secret = get_secret_by_name(name) - if not secret: - logger.warning(f"Secret not found: {name}") - raise HTTPException(status_code=404, detail=f"Secret '{name}' not found") - - # Get detailed description - description = describe_secret(name) - - if description is None: - logger.warning(f"Could not retrieve detailed description for secret: {name}") - raise HTTPException(status_code=404, detail=f"Could not retrieve detailed description for secret '{name}'") - - return description - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error getting detailed secret description: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - except Exception as e: - logger.error(f"Error in get_secret: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/api/v1/endpoints/services.py b/opt/torero-api/torero_api/api/v1/endpoints/services.py deleted file mode 100644 index bc590b6..0000000 --- a/opt/torero-api/torero_api/api/v1/endpoints/services.py +++ /dev/null @@ -1,354 +0,0 @@ -""" -Services endpoints for torero API - -This module defines the API endpoints for interacting with torero services. -""" - -from fastapi import APIRouter, HTTPException, Query, Path, Depends -from typing import Optional, List -import logging - -from torero_api.models.service import Service, ServiceType -from torero_api.core.torero_executor import get_services, get_service_by_name, describe_service - -# Set up logging -logger = logging.getLogger(__name__) - -# Create router -router = APIRouter() - -# Adding pagination parameters -def common_parameters( - skip: int = Query(0, ge=0, description="Number of items to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of items to return") -): - """ - Common pagination parameters for endpoints that return lists. - - Args: - skip: Number of items to skip - limit: Maximum number of items to return - - Returns: - dict: Dictionary containing the pagination parameters - """ - return {"skip": skip, "limit": limit} - -@router.get( - "/", - response_model=List[Service], - summary="List services", - description=""" - Get all registered torero services with optional filtering. - - This endpoint returns a list of all services registered with torero. - You can filter the results by service type and/or tag to narrow down - the list to specific services of interest. - - Examples: - - List all services: GET /v1/services/ - - List all ansible playbooks: GET /v1/services/?type=ansible-playbook - - List services with "network" tag: GET /v1/services/?tag=network - - Combine filters: GET /v1/services/?type=ansible-playbook&tag=network - """ -) -def list_services( - commons: dict = Depends(common_parameters), - type: Optional[ServiceType] = Query( - None, - description="Filter by service type, e.g. 'ansible-playbook'", - examples={ - "ansible-playbook": { - "summary": "Ansible Playbook", - "description": "Filter for only Ansible Playbook services", - "value": "ansible-playbook" - }, - "opentofu-plan": { - "summary": "OpenTofu Plan", - "description": "Filter for only OpenTofu Plan services", - "value": "opentofu-plan" - }, - "python-script": { - "summary": "Python Script", - "description": "Filter for only Python Script services", - "value": "python-script" - } - } - ), - tag: Optional[str] = Query( - None, - description="Filter by tag (e.g., 'network', 'backup')", - examples={ - "network": { - "summary": "Network Tag", - "description": "Filter services with the 'network' tag", - "value": "network" - }, - "backup": { - "summary": "Backup Tag", - "description": "Filter services with the 'backup' tag", - "value": "backup" - }, - "automation": { - "summary": "Automation Tag", - "description": "Filter services with the 'automation' tag", - "value": "automation" - } - } - ) -): - """ - List all torero services, optionally filtered by type and/or tag. - - Args: - commons: Common pagination parameters - type: Optional filter to return only services of a specific type - tag: Optional filter to return only services with a specific tag - - Returns: - List[Service]: List of Service objects matching the filter criteria - - Raises: - HTTPException: If an error occurs while retrieving or filtering services - """ - try: - logger.info(f"Getting services with filters - type: {type}, tag: {tag}") - services = get_services() - - # Apply filters if provided - if type: - services = [s for s in services if s.type == type] - if tag: - services = [s for s in services if tag in s.tags] - - # Apply pagination - skip = commons["skip"] - limit = commons["limit"] - paginated_services = services[skip:skip + limit] - - logger.info(f"Returning {len(paginated_services)} services after filtering") - return paginated_services - - except Exception as e: - logger.error(f"Error in list_services: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/types", - response_model=List[str], - summary="List service types", - description=""" - Return a list of unique service types used by registered services. - - This endpoint provides a list of all distinct service types (e.g., ansible-playbook, - opentofu-plan, python-script) that are currently in use across all registered services. - - This information is useful for: - - Building UI dropdown filters - - Understanding what types of services are available - - Validating type values for new services - """ -) -def list_service_types(): - """ - Return a list of unique service types used by registered services. - - Retrieves all services and extracts the unique set of service types, - returning them as a sorted list. - - Returns: - List[str]: Sorted list of unique service type strings - - Raises: - HTTPException: If an error occurs while retrieving services - """ - try: - logger.info("Getting service types") - services = get_services() - types = sorted(set(s.type for s in services)) - logger.info(f"Returning {len(types)} service types") - return types - except Exception as e: - logger.error(f"Error in list_service_types: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/tags", - response_model=List[str], - summary="List service tags", - description=""" - Return a list of unique tags used across all registered services. - - This endpoint provides a list of all distinct tags that are currently - applied to registered services. Services can have multiple tags, and - this endpoint aggregates them into a single, deduplicated list. - - This information is useful for: - - Building tag clouds or filter interfaces - - Understanding how services are categorized - - Discovering available service categories - """ -) -def list_service_tags(): - """ - Return a list of unique tags used across all registered services. - - Retrieves all services, extracts all tags from each service, - and returns the unique set of tags as a sorted list. - - Returns: - List[str]: Sorted list of unique tag strings - - Raises: - HTTPException: If an error occurs while retrieving services - """ - try: - logger.info("Getting service tags") - services = get_services() - tags = sorted(set(tag for s in services for tag in s.tags)) - logger.info(f"Returning {len(tags)} service tags") - return tags - except Exception as e: - logger.error(f"Error in list_service_tags: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}", - response_model=Service, - summary="Get service details", - description=""" - Get detailed information about a specific service by name. - - This endpoint retrieves detailed information about a single service - identified by its name. The name is case-sensitive and must match - exactly the name of a registered service. - - If no service is found with the specified name, a 404 error is returned. - """ -) -def get_service( - name: str = Path( - ..., - description="Name of the service to retrieve", - examples={ - "network-backup": { - "summary": "Network Backup Service", - "description": "Get details for a network backup service", - "value": "network-backup" - }, - "app-deployment": { - "summary": "Application Deployment Service", - "description": "Get details for an application deployment service", - "value": "app-deployment" - }, - "system-monitor": { - "summary": "System Monitoring Service", - "description": "Get details for a system monitoring service", - "value": "system-monitor" - } - } - ) -): - """ - Get detailed information about a specific service by name. - - Args: - name: The name of the service to retrieve - - Returns: - Service: The service with the specified name - - Raises: - HTTPException: If the service is not found or an error occurs - """ - try: - logger.info(f"Getting service details for: {name}") - service = get_service_by_name(name) - - if service: - logger.info(f"Found service: {name}") - return service - - # If we get here, the service was not found - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except Exception as e: - logger.error(f"Error in get_service: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get( - "/{name}/describe", - summary="Get detailed service description", - description=""" - Get comprehensive detailed description of a specific service by name. - - This endpoint uses 'torero describe service' to retrieve extensive - information about a service including metadata, entity details, - playbook options, and configuration. - - If no service is found with the specified name, a 404 error is returned. - """ -) -def describe_service_endpoint( - name: str = Path( - ..., - description="Name of the service to describe", - examples={ - "cisco-nxos-vlan-config": { - "summary": "Cisco NXOS VLAN Config Service", - "description": "Get detailed description for VLAN configuration service", - "value": "cisco-nxos-vlan-config" - } - } - ) -): - """ - Get comprehensive detailed description of a specific service by name. - - Args: - name: The name of the service to describe - - Returns: - dict: Detailed service description with metadata and entity information - - Raises: - HTTPException: If the service is not found or an error occurs - """ - try: - logger.info(f"Getting detailed description for service: {name}") - - # First verify the service exists - service = get_service_by_name(name) - if not service: - logger.warning(f"Service not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - - # Get detailed description - description = describe_service(name) - - if description: - logger.info(f"Found detailed description for service: {name}") - return description - - # If we get here, the service description was not found - logger.warning(f"Service description not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' description not available") - - except HTTPException: - # Re-raise HTTP exceptions - raise - except RuntimeError as e: - # Handle specific RuntimeError from describe_service - if "not found" in str(e).lower() or "404" in str(e): - logger.warning(f"Service description not found: {name}") - raise HTTPException(status_code=404, detail=f"Service '{name}' not found") - else: - logger.error(f"Error in describe_service_endpoint: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) - except Exception as e: - logger.error(f"Error in describe_service_endpoint: {str(e)}") - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/opt/torero-api/torero_api/core/__init__.py b/opt/torero-api/torero_api/core/__init__.py deleted file mode 100644 index 732b0ce..0000000 --- a/opt/torero-api/torero_api/core/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Core functionality for the torero API - -This package contains the core business logic and utility functions -for the torero API, including the interface to the torero CLI. - -The core package abstracts the implementation details of interacting -with torero, allowing the API layer to focus on request handling and -response formatting. - -Components: -- torero_executor: Interface for executing torero CLI commands and - parsing their output into structured data -""" - -# Re-export core components for easier imports -from torero_api.core.torero_executor import ( - check_torero_available, - check_torero_version, - get_services, - get_service_by_name, - describe_service, - get_decorators, - get_decorator_by_name, - get_repositories, - get_repository_by_name, - get_secrets, - get_secret_by_name, - run_ansible_playbook_service, - run_python_script_service -) \ No newline at end of file diff --git a/opt/torero-api/torero_api/core/torero_executor.py b/opt/torero-api/torero_api/core/torero_executor.py deleted file mode 100644 index 5ee0545..0000000 --- a/opt/torero-api/torero_api/core/torero_executor.py +++ /dev/null @@ -1,1178 +0,0 @@ -""" -torero executor module - -This module provides functions to interact with the torero CLI. -It's responsible for executing torero commands and parsing their output. -""" - -import json -import logging -import subprocess -import shutil -from typing import List, Tuple, Optional -from datetime import datetime - -from torero_api.models.service import Service -from torero_api.models.database import DatabaseImportOptions - -# Configure logging -logger = logging.getLogger(__name__) - -# torero command -TORERO_COMMAND = 'torero' - -def check_torero_available() -> Tuple[bool, str]: - """ - Check if torero is available in the system PATH. - - Returns: - Tuple[bool, str]: A tuple containing a boolean indicating whether torero is available - and a message with more details - """ - - # Check if torero executable is in PATH - torero_path = shutil.which(TORERO_COMMAND) - if not torero_path: - return False, f"{TORERO_COMMAND} executable not found in PATH" - - # Check if torero can be executed - try: - result = subprocess.run( - [TORERO_COMMAND, "version"], - capture_output=True, - text=True, - check=False, - timeout=5 - ) - - if result.returncode != 0: - return False, f"{TORERO_COMMAND} command failed: {result.stderr.strip()}" - - return True, f"{TORERO_COMMAND} is available" - except subprocess.TimeoutExpired: - return False, f"{TORERO_COMMAND} command timed out" - except Exception as e: - return False, f"Error checking {TORERO_COMMAND}: {str(e)}" - -def check_torero_version() -> str: - """ - Get the version of torero installed. - - Returns: - str: The version of torero, or "unknown" if it couldn't be determined - """ - try: - result = subprocess.run( - [TORERO_COMMAND, "version"], - capture_output=True, - text=True, - check=False, - timeout=5 - ) - - if result.returncode != 0: - return "unknown" - - # Parse the version from the output - # Example output: "torero version 1.3.1" - output_lines = result.stdout.strip().split("\n") - for line in output_lines: - if line.startswith("torero"): - parts = line.split() - if len(parts) >= 3: - return parts[2] - - return "unknown" - except Exception: - return "unknown" - -def get_services() -> List[Service]: - """ - Execute torero CLI command to get all services. - - Makes a system call to 'torero get services --raw' to retrieve the raw JSON - data of all registered services, then parses and validates this data into - Service objects. - - Returns: - List[Service]: List of Service objects representing all registered torero services. - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "get", "services", "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # Handle the case where services are wrapped in an "items" array - if isinstance(raw_output, dict) and "items" in raw_output: - services_data = raw_output["items"] - elif isinstance(raw_output, list): - services_data = raw_output - else: - raise RuntimeError(f"Unexpected JSON structure from torero: {type(raw_output)}") - - # Create Service objects - services = [Service(**svc) for svc in services_data] - logger.debug(f"Retrieved {len(services)} services from torero") - return services - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") # Log first 1000 chars - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero command: {str(e)}") - raise RuntimeError(f"Failed to execute torero command: {str(e)}") - -def get_service_by_name(name: str) -> Optional[Service]: - """ - Get a specific service by name. - - Args: - name: The name of the service to retrieve - - Returns: - Optional[Service]: The service if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - services = get_services() - - for service in services: - if service.name == name: - return service - - return None - -def describe_service(name: str) -> Optional[dict]: - """ - Get detailed description of a specific service by name. - - Makes a system call to 'torero describe service --raw' to retrieve - detailed information about a specific service. - - Args: - name: The name of the service to describe - - Returns: - Optional[dict]: Detailed service information if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "describe", "service", name, "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # The describe command returns an array, return the full response - if isinstance(raw_output, list): - logger.debug(f"Retrieved detailed description for service: {name}") - return raw_output - elif isinstance(raw_output, dict): - logger.debug(f"Retrieved detailed description for service: {name}") - return [raw_output] # Wrap single dict in array for consistency - else: - logger.warning(f"Unexpected response format for service description: {name}") - return None - - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero describe: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero describe command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero describe command: {str(e)}") - raise RuntimeError(f"Failed to execute torero describe command: {str(e)}") - -def get_decorators() -> List['Decorator']: - """ - Execute torero CLI command to get all decorators. - - Makes a system call to 'torero get decorators --raw' to retrieve the raw JSON - data of all registered decorators, then parses and validates this data into - Decorator objects. - - Returns: - List[Decorator]: List of Decorator objects representing all registered torero decorators. - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "get", "decorators", "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # Handle the case where decorators are wrapped in a "decorators" array - if isinstance(raw_output, dict) and "decorators" in raw_output: - decorators_data = raw_output["decorators"] - elif isinstance(raw_output, dict) and "items" in raw_output: - decorators_data = raw_output["items"] - elif isinstance(raw_output, list): - decorators_data = raw_output - else: - raise RuntimeError(f"Unexpected JSON structure from torero: {type(raw_output)}") - - # Create Decorator objects - from torero_api.models.decorator import Decorator - decorators = [] - - for decorator_data in decorators_data: - # Map torero CLI fields to Decorator model fields - # The CLI uses "schema" but our model expects "parameters" - decorator_info = { - "name": decorator_data.get("name", "unknown"), - "description": decorator_data.get("description") or None, - "type": decorator_data.get("type", "decorator"), # Use provided type or default to "decorator" - "parameters": decorator_data.get("schema") or decorator_data.get("parameters"), # Map schema to parameters, fallback to parameters - "registries": { - "metadata": { - "id": decorator_data.get("id"), - "created": decorator_data.get("created"), - "tags": decorator_data.get("tags", []) - } - } - } - - decorators.append(Decorator(**decorator_info)) - - logger.debug(f"Retrieved {len(decorators)} decorators from torero") - return decorators - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero command: {str(e)}") - raise RuntimeError(f"Failed to execute torero command: {str(e)}") - -def get_decorator_by_name(name: str) -> Optional['Decorator']: - """ - Get a specific decorator by name. - - Args: - name: The name of the decorator to retrieve - - Returns: - Optional[Decorator]: The decorator if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - decorators = get_decorators() - - for decorator in decorators: - if decorator.name == name: - return decorator - - return None - -def get_repositories() -> List['Repository']: - """ - Execute torero CLI command to get all repositories. - - Makes a system call to 'torero get repositories --raw' to retrieve the raw JSON - data of all registered repositories, then parses and validates this data into - Repository objects. - - Returns: - List[Repository]: List of Repository objects representing all registered torero repositories. - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "get", "repositories", "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # Handle the case where repositories are wrapped in an "items" array - if isinstance(raw_output, dict) and "items" in raw_output: - repositories_data = raw_output["items"] - elif isinstance(raw_output, list): - repositories_data = raw_output - else: - raise RuntimeError(f"Unexpected JSON structure from torero: {type(raw_output)}") - - # Create Repository objects - from torero_api.models.repository import Repository - repositories = [] - - for repo_data in repositories_data: - # Map torero CLI fields to Repository model fields - repository_info = { - "name": repo_data.get("name", "unknown"), - "description": repo_data.get("description"), - "type": repo_data.get("type") or ("git" if repo_data.get("url", "").endswith(".git") else "unknown"), - "location": repo_data.get("url") or repo_data.get("location", "unknown"), - "metadata": { - "reference": repo_data.get("reference"), - "tags": repo_data.get("tags", []), - "private_key_name": repo_data.get("private_key_name", "") - } - } - - repositories.append(Repository(**repository_info)) - - logger.debug(f"Retrieved {len(repositories)} repositories from torero") - return repositories - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero command: {str(e)}") - raise RuntimeError(f"Failed to execute torero command: {str(e)}") - -def get_repository_by_name(name: str) -> Optional['Repository']: - """ - Get a specific repository by name. - - Args: - name: The name of the repository to retrieve - - Returns: - Optional[Repository]: The repository if found, None otherwise - - Raises: - RuntimeError: If the torero command fails. - """ - repositories = get_repositories() - - for repository in repositories: - if repository.name == name: - return repository - - return None - -def get_secrets() -> List['Secret']: - """ - Execute torero CLI command to get all secrets. - - Makes a system call to 'torero get secrets --raw' to retrieve the raw JSON - data of all registered secrets, then parses and validates this data into - Secret objects. - - Returns: - List[Secret]: List of Secret objects representing all registered torero secrets. - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "get", "secrets", "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # Handle the case where secrets are wrapped in an "items" array - if isinstance(raw_output, dict) and "items" in raw_output: - secrets_data = raw_output["items"] - elif isinstance(raw_output, list): - secrets_data = raw_output - else: - raise RuntimeError(f"Unexpected JSON structure from torero: {type(raw_output)}") - - # Create Secret objects - from torero_api.models.secret import Secret - secrets = [Secret(**secret) for secret in secrets_data] - logger.debug(f"Retrieved {len(secrets)} secrets from torero") - return secrets - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero command: {str(e)}") - raise RuntimeError(f"Failed to execute torero command: {str(e)}") - -def get_secret_by_name(name: str) -> Optional['Secret']: - """ - Get a specific secret by name. - - Args: - name: The name of the secret to retrieve - - Returns: - Optional[Secret]: The secret if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - secrets = get_secrets() - - for secret in secrets: - if secret.name == name: - return secret - - return None - -def run_ansible_playbook_service(name: str, **kwargs) -> dict: - """ - Execute an Ansible playbook service using torero. - - Makes a system call to 'torero run service ansible-playbook --raw' - to run an Ansible playbook service and return its execution results. - - Args: - name: The name of the Ansible playbook service to run - **kwargs: Additional parameters to pass to the service - - Returns: - dict: Execution results including return code, stdout, stderr, and timing information - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "run", "service", "ansible-playbook", name, "--raw"] - - # Add any additional parameters as command arguments - for key, value in kwargs.items(): - if value is not None: - command.append(f"--{key}={value}") - - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=300 # 5 minute timeout for playbook execution - ) - - try: - # Parse the output as JSON - result = json.loads(proc.stdout) - logger.debug(f"Successfully executed Ansible playbook service: {name}") - return result - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero run service: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") # Log first 1000 chars - - # If we can't parse JSON but have a non-zero return code, it's likely an error - if proc.returncode != 0: - error_msg = f"Service execution failed with code {proc.returncode}: {proc.stderr.strip()}" - - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "Service execution timed out after 5 minutes" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing service: {str(e)}") - raise RuntimeError(f"Failed to execute service: {str(e)}") - -def run_python_script_service(name: str, **kwargs) -> dict: - """ - Execute a Python script service using torero. - - Makes a system call to 'torero run service python-script --raw' - to run a Python script service and return its execution results. - - Args: - name: The name of the Python script service to run - **kwargs: Additional parameters to pass to the service - - Returns: - dict: Execution results including return code, stdout, stderr, and timing information - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "run", "service", "python-script", name, "--raw"] - - # Add any additional parameters as command arguments - for key, value in kwargs.items(): - if value is not None: - command.append(f"--{key}={value}") - - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=300 # 5 minute timeout for script execution - ) - - try: - # Parse the output as JSON - result = json.loads(proc.stdout) - logger.debug(f"Successfully executed Python script service: {name}") - return result - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero run service: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") # Log first 1000 chars - - # If we can't parse JSON but have a non-zero return code, it's likely an error - if proc.returncode != 0: - error_msg = f"Service execution failed with code {proc.returncode}: {proc.stderr.strip()}" - - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "Service execution timed out after 5 minutes" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing service: {str(e)}") - raise RuntimeError(f"Failed to execute service: {str(e)}") - -def run_opentofu_plan_apply_service(name: str, **kwargs) -> dict: - """ - Execute an OpenTofu plan apply service using torero. - - Makes a system call to 'torero run service opentofu-plan apply --raw' - to apply an OpenTofu plan service and return its execution results. - - Args: - name: The name of the OpenTofu plan service to apply - **kwargs: Additional parameters to pass to the service - - Returns: - dict: Execution results including return code, stdout, stderr, and timing information - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "run", "service", "opentofu-plan", "apply", name, "--raw"] - - # Add any additional parameters as command arguments - for key, value in kwargs.items(): - if value is not None: - command.append(f"--{key}={value}") - - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=600 # 10 minute timeout for plan apply - ) - - try: - # Parse the output as JSON - result = json.loads(proc.stdout) - logger.debug(f"Successfully applied OpenTofu plan service: {name}") - return result - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero run service: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") # Log first 1000 chars - - # If we can't parse JSON but have a non-zero return code, it's likely an error - if proc.returncode != 0: - error_msg = f"Service execution failed with code {proc.returncode}: {proc.stderr.strip()}" - - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "Service execution timed out after 10 minutes" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing service: {str(e)}") - raise RuntimeError(f"Failed to execute service: {str(e)}") - -def describe_repository(name: str) -> Optional[dict]: - """ - Get detailed description of a specific repository by name. - - Makes a system call to 'torero describe repository --raw' to retrieve - detailed information about a specific repository. - - Args: - name: The name of the repository to describe - - Returns: - Optional[dict]: Detailed repository information if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "describe", "repository", name, "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # The describe command returns detailed info, return the full response - logger.debug(f"Retrieved detailed description for repository: {name}") - return raw_output - - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero describe: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero describe command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero describe command: {str(e)}") - raise RuntimeError(f"Failed to execute torero describe command: {str(e)}") - -def describe_secret(name: str) -> Optional[dict]: - """ - Get detailed description of a specific secret by name. - - Makes a system call to 'torero describe secret --raw' to retrieve - detailed information about a specific secret. - - Args: - name: The name of the secret to describe - - Returns: - Optional[dict]: Detailed secret information if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "describe", "secret", name, "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # The describe command returns detailed info, return the full response - logger.debug(f"Retrieved detailed description for secret: {name}") - return raw_output - - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero describe: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero describe command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero describe command: {str(e)}") - raise RuntimeError(f"Failed to execute torero describe command: {str(e)}") - -def get_registries() -> List['Registry']: - """ - Execute torero CLI command to get all registries. - - Makes a system call to 'torero get registries --raw' to retrieve the raw JSON - data of all registered registries, then parses and validates this data into - Registry objects. - - Returns: - List[Registry]: List of Registry objects representing all registered torero registries. - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "get", "registries", "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # Handle the case where registries are wrapped in an "items" array - if isinstance(raw_output, dict) and "items" in raw_output: - registries_data = raw_output["items"] - elif isinstance(raw_output, list): - registries_data = raw_output - else: - raise RuntimeError(f"Unexpected JSON structure from torero: {type(raw_output)}") - - # Create Registry objects - from torero_api.models.registry import Registry - registries = [] - - for reg_data in registries_data: - # Map torero CLI fields to Registry model fields - registry_info = { - "name": reg_data.get("name", "unknown"), - "description": reg_data.get("description"), - "type": reg_data.get("type", "unknown"), - "url": reg_data.get("url", ""), - "metadata": { - "id": reg_data.get("id"), - "created": reg_data.get("created"), - "tags": reg_data.get("tags", []), - "credentials": reg_data.get("credentials") - } - } - - registries.append(Registry(**registry_info)) - - logger.debug(f"Retrieved {len(registries)} registries from torero") - return registries - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero command: {str(e)}") - raise RuntimeError(f"Failed to execute torero command: {str(e)}") - -def get_registry_by_name(name: str) -> Optional['Registry']: - """ - Get a specific registry by name. - - Args: - name: The name of the registry to retrieve - - Returns: - Optional[Registry]: The registry if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - registries = get_registries() - - for registry in registries: - if registry.name == name: - return registry - - return None - -def describe_decorator(name: str) -> Optional[dict]: - """ - Get detailed description of a specific decorator by name. - - Makes a system call to 'torero describe decorator --raw' to retrieve - detailed information about a specific decorator. - - Args: - name: The name of the decorator to describe - - Returns: - Optional[dict]: Detailed decorator information if found, None otherwise - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "describe", "decorator", name, "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=30 - ) - - if proc.returncode != 0: - error_msg = f"torero error: {proc.stderr.strip()}" - logger.error(error_msg) - raise RuntimeError(error_msg) - - try: - # Parse the output as JSON - raw_output = json.loads(proc.stdout) - - # The describe command returns detailed info, return the full response - logger.debug(f"Retrieved detailed description for decorator: {name}") - return raw_output - - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero describe: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "torero describe command timed out" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing torero describe command: {str(e)}") - raise RuntimeError(f"Failed to execute torero describe command: {str(e)}") - -def run_opentofu_plan_destroy_service(name: str, **kwargs) -> dict: - """ - Execute an OpenTofu plan destroy service using torero. - - Makes a system call to 'torero run service opentofu-plan destroy --raw' - to destroy resources managed by an OpenTofu plan service and return its execution results. - - Args: - name: The name of the OpenTofu plan service to destroy - **kwargs: Additional parameters to pass to the service - - Returns: - dict: Execution results including return code, stdout, stderr, and timing information - - Raises: - RuntimeError: If the torero command fails or returns invalid JSON. - """ - command = [TORERO_COMMAND, "run", "service", "opentofu-plan", "destroy", name, "--raw"] - - # Add any additional parameters as command arguments - for key, value in kwargs.items(): - if value is not None: - command.append(f"--{key}={value}") - - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=600 - ) - - try: - # Parse the output as JSON - result = json.loads(proc.stdout) - logger.debug(f"Successfully destroyed OpenTofu plan service: {name}") - return result - except json.JSONDecodeError as e: - error_msg = f"Invalid JSON from torero run service: {e}" - logger.error(error_msg) - logger.debug(f"Raw output: {proc.stdout[:1000]}...") - - # If we can't parse JSON but have a non-zero return code, it's likely an error - if proc.returncode != 0: - error_msg = f"Service execution failed with code {proc.returncode}: {proc.stderr.strip()}" - - raise RuntimeError(error_msg) - - except subprocess.TimeoutExpired: - error_msg = "Service execution timed out after 10 minutes" - logger.error(error_msg) - raise RuntimeError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing service: {str(e)}") - raise RuntimeError(f"Failed to execute service: {str(e)}") - -class ToreroError(Exception): - """Custom exception for torero-related errors.""" - pass - -async def execute_db_export(format: str = "yaml") -> dict: - """ - Execute torero db export command. - - Makes a system call to 'torero db export --format --raw' to export - all services and resources. - - Args: - format: The output format (json or yaml). Defaults to yaml. - - Returns: - dict: The exported configuration data. - - Raises: - ToreroError: If the export command fails. - """ - command = [TORERO_COMMAND, "db", "export", "--format", format, "--raw"] - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=60 - ) - - if proc.returncode != 0: - error_msg = f"torero db export error: {proc.stderr.strip()}" - logger.error(error_msg) - raise ToreroError(error_msg) - - try: - # For YAML format, the output might be a string - if format == "yaml": - # Return the raw YAML string - return {"data": proc.stdout, "format": "yaml"} - else: - # Parse JSON output - result = json.loads(proc.stdout) - return result - except json.JSONDecodeError as e: - # If JSON parsing fails, return as string - return {"data": proc.stdout, "format": format} - - except subprocess.TimeoutExpired: - error_msg = "torero db export command timed out" - logger.error(error_msg) - raise ToreroError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing db export: {str(e)}") - raise ToreroError(f"Failed to execute db export: {str(e)}") - -async def execute_db_import(file_path: str, options: DatabaseImportOptions) -> dict: - """ - Execute torero db import command. - - Makes a system call to 'torero db import' with various options to import - services and resources from a file or repository. - - Args: - file_path: Path to the import file (local or within repository). - options: Import options including repository, force, check, validate. - - Returns: - dict: Import result including status and any conflicts or changes. - - Raises: - ToreroError: If the import command fails. - """ - command = [TORERO_COMMAND, "db", "import"] - - # Add repository options if specified - if options.repository: - command.extend(["--repository", options.repository]) - if options.reference: - command.extend(["--reference", options.reference]) - if options.private_key: - command.extend(["--private-key-name", options.private_key]) - - # Add flags - if options.force: - command.append("--force") - if options.check: - command.append("--check") - if options.validate_only: - command.append("--validate") - - # Add file path - command.append(file_path) - - # Always use raw output - command.append("--raw") - - logger.debug(f"Executing command: {' '.join(command)}") - - try: - # Run the torero command - proc = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - timeout=120 - ) - - # Import might return non-zero for conflicts, but still have useful output - if proc.returncode != 0 and not proc.stdout: - error_msg = f"torero db import error: {proc.stderr.strip()}" - logger.error(error_msg) - raise ToreroError(error_msg) - - try: - # Parse the output as JSON - if proc.stdout: - result = json.loads(proc.stdout) - return result - else: - # If no stdout but stderr has info, use that - return { - "success": proc.returncode == 0, - "message": proc.stderr.strip() if proc.stderr else "Import completed" - } - except json.JSONDecodeError: - # If not JSON, return structured response - return { - "success": proc.returncode == 0, - "message": proc.stdout.strip() if proc.stdout else proc.stderr.strip() - } - - except subprocess.TimeoutExpired: - error_msg = "torero db import command timed out" - logger.error(error_msg) - raise ToreroError(error_msg) - except Exception as e: - logger.exception(f"Unexpected error executing db import: {str(e)}") - raise ToreroError(f"Failed to execute db import: {str(e)}") \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/__init__.py b/opt/torero-api/torero_api/models/__init__.py deleted file mode 100644 index 3494b2e..0000000 --- a/opt/torero-api/torero_api/models/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Data models for the torero API - -This package contains Pydantic models that define the data structures -used throughout the torero API. These models provide type validation, -serialization/deserialization, and documentation through OpenAPI schemas. - -The models are designed to be MCP-compatible, providing rich type -information and examples to assist AI systems in understanding the API. - -Models: -- Service: Represents a torero service with its metadata -- Decorator: Represents a torero decorator with its metadata -- Repository: Represents a torero repository with its metadata -- Secret: Represents a torero secret with its metadata -- ErrorResponse: Standard error response format -- APIInfo: Information about the API and available endpoints -- ServiceExecutionResult: Result of a service execution -""" - -# Re-export models for easier imports -from torero_api.models.service import Service, ServiceType -from torero_api.models.decorator import Decorator -from torero_api.models.repository import Repository -from torero_api.models.secret import Secret -from torero_api.models.common import ErrorResponse, APIInfo -from torero_api.models.execution import ServiceExecutionResult \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/common.py b/opt/torero-api/torero_api/models/common.py deleted file mode 100644 index baeb5aa..0000000 --- a/opt/torero-api/torero_api/models/common.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Common models for torero API - -This module defines common models used throughout the API. -""" - -from typing import Dict, Optional -from pydantic import BaseModel, Field - -class ErrorResponse(BaseModel): - """ - Standard error response model for API errors. - - Attributes: - status_code: HTTP status code indicating the error type - detail: Human-readable error message providing error details - error_type: Categorization of the error for programmatic handling - path: API endpoint where the error occurred - """ - status_code: int = Field(..., description="HTTP status code") - detail: str = Field(..., description="Error message") - error_type: str = Field(..., description="Error category") - path: str = Field(..., description="API endpoint path") - - # Updated Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "status_code": 400, - "detail": "Invalid request parameters", - "error_type": "validation_error", - "path": "/v1/services/" - } - } - } - -class APIInfo(BaseModel): - """ - Information about the API for the root endpoint response. - - Provides metadata about the API and navigation links to available endpoints. - """ - name: str = Field(..., description="API name") - version: str = Field(..., description="API version") - description: str = Field(..., description="API description") - documentation: str = Field(..., description="URL to API documentation") - endpoints: Dict[str, str] = Field(..., description="Available API endpoints") - - # Updated Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "name": "torero API", - "version": "0.1.0", - "description": "API for torero services", - "documentation": "http://localhost:8000/docs", - "endpoints": { - "services": "http://localhost:8000/v1/services/", - "service_types": "http://localhost:8000/v1/services/types", - "service_tags": "http://localhost:8000/v1/services/tags" - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/database.py b/opt/torero-api/torero_api/models/database.py deleted file mode 100644 index bfc221b..0000000 --- a/opt/torero-api/torero_api/models/database.py +++ /dev/null @@ -1,51 +0,0 @@ -from enum import Enum -from typing import Optional, List, Dict, Any -from pydantic import BaseModel, Field - -class DatabaseExportFormat(str, Enum): - """Supported export formats for database export.""" - JSON = "json" - YAML = "yaml" - -class DatabaseImportOptions(BaseModel): - """Options for database import operations.""" - repository: Optional[str] = Field(None, description="Repository URL for remote import") - reference: Optional[str] = Field(None, description="Branch, tag, or commit reference") - private_key: Optional[str] = Field(None, description="Private key name for SSH authentication") - force: bool = Field(False, description="Override existing services") - check: bool = Field(False, description="Perform dry-run validation") - validate_only: bool = Field(False, description="Validate service file only") - -class ImportCheckState(str, Enum): - """States for resources during import check.""" - CONFLICT = "Conflict" - ADD = "Add" - REPLACEMENT = "Replacement" - -class ImportCheckItem(BaseModel): - """Individual item in import check result.""" - name: str = Field(..., description="Resource name") - type: str = Field(..., description="Resource type") - state: ImportCheckState = Field(..., description="Import state") - message: Optional[str] = Field(None, description="Additional information") - -class DatabaseImportCheckResult(BaseModel): - """Result of import check operation.""" - conflicts: List[ImportCheckItem] = Field(default_factory=list, description="Resources that would conflict") - additions: List[ImportCheckItem] = Field(default_factory=list, description="Resources that would be added") - replacements: List[ImportCheckItem] = Field(default_factory=list, description="Resources that would be replaced") - summary: Dict[str, int] = Field(default_factory=dict, description="Summary counts by type") - -class DatabaseImportResult(BaseModel): - """Result of database import operation.""" - success: bool = Field(..., description="Whether import was successful") - imported: Dict[str, int] = Field(default_factory=dict, description="Count of imported resources by type") - conflicts: Optional[List[str]] = Field(None, description="List of conflicts if any") - message: Optional[str] = Field(None, description="Result message") - -class DatabaseExportResult(BaseModel): - """Result of database export operation.""" - decorators: Optional[List[Dict[str, Any]]] = Field(None, description="Exported decorators") - repositories: Optional[List[Dict[str, Any]]] = Field(None, description="Exported repositories") - services: Optional[List[Dict[str, Any]]] = Field(None, description="Exported services") - metadata: Optional[Dict[str, Any]] = Field(None, description="Export metadata") \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/decorator.py b/opt/torero-api/torero_api/models/decorator.py deleted file mode 100644 index e6a7622..0000000 --- a/opt/torero-api/torero_api/models/decorator.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Decorator model for torero API - -This module defines the Decorator model, which represents a torero decorator. -""" - -from typing import List, Optional, Dict, Any -from pydantic import BaseModel, Field - -class Decorator(BaseModel): - """ - Represents a torero decorator. - - Decorators modify the behavior of torero services by adding functionality - such as authentication, logging, or parameter validation. - - Attributes: - name: Unique identifier for the decorator - description: Human-readable explanation of the decorator's purpose - type: Category of decorator indicating its function - parameters: Schema for parameters accepted by the decorator - registries: Optional metadata about where the decorator is registered - """ - name: str = Field( - ..., - description="Unique identifier for the decorator" - ) - description: Optional[str] = Field( - None, - description="Human-readable explanation of the decorator's purpose" - ) - type: str = Field( - ..., - description="Category of decorator indicating its function" - ) - parameters: Optional[Dict[str, Any]] = Field( - None, - description="Schema for parameters accepted by the decorator" - ) - registries: Optional[dict] = Field( - None, - description="Metadata about where the decorator is registered" - ) - - # Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "name": "auth-basic", - "description": "Adds HTTP Basic Authentication to service calls", - "type": "authentication", - "parameters": { - "username": {"type": "string", "required": True}, - "password": {"type": "string", "required": True, "secret": True} - }, - "registries": { - "file": { - "path": "/etc/torero/decorators/auth-basic" - } - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/execution.py b/opt/torero-api/torero_api/models/execution.py deleted file mode 100644 index ac1838d..0000000 --- a/opt/torero-api/torero_api/models/execution.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Execution models for torero API - -This module defines models related to service execution results. -""" - -from typing import Optional -from datetime import datetime -from pydantic import BaseModel, Field - -class ServiceExecutionResult(BaseModel): - """ - Result of a service execution. - - Represents the output of running a torero service. - - Attributes: - return_code: The exit code returned by the executed service - stdout: Standard output captured during execution - stderr: Standard error output captured during execution - start_time: ISO 8601 timestamp when execution started - end_time: ISO 8601 timestamp when execution completed - elapsed_time: Execution duration in seconds - """ - return_code: int = Field(..., description="Exit code from the execution") - stdout: str = Field(..., description="Standard output from the execution") - stderr: str = Field(..., description="Standard error output from the execution") - start_time: str = Field(..., description="ISO 8601 timestamp when execution started") - end_time: str = Field(..., description="ISO 8601 timestamp when execution completed") - elapsed_time: float = Field(..., description="Execution duration in seconds") - - # Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "return_code": 0, - "stdout": "\nPLAY [Hello World] *************************************************************\n\nTASK [Gathering Facts] *********************************************************\nok: [127.0.0.1]\n\nTASK [Ping my hosts] ***********************************************************\nok: [127.0.0.1]\n\nTASK [Print message] ***********************************************************\nok: [127.0.0.1] => {\n \"msg\": \"Hello world!\"\n}\n\nPLAY RECAP *********************************************************************\n127.0.0.1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 \n\n", - "stderr": "[WARNING]: No inventory was parsed, only implicit localhost is available\n[WARNING]: provided hosts list is empty, only localhost is available. Note that\nthe implicit localhost does not match 'all'\n", - "start_time": "2025-05-26T22:18:41.905955Z", - "end_time": "2025-05-26T22:18:45.034007Z", - "elapsed_time": 3.1280594 - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/registry.py b/opt/torero-api/torero_api/models/registry.py deleted file mode 100644 index 99f0784..0000000 --- a/opt/torero-api/torero_api/models/registry.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Registry models for torero API - -This module defines the data models for torero registries. -""" - -from pydantic import BaseModel, Field -from typing import Optional, Dict, Any, List -from datetime import datetime - - -class RegistryMetadata(BaseModel): - """Metadata information for a registry""" - id: Optional[str] = Field(None, description="Unique identifier for the registry") - created: Optional[datetime] = Field(None, description="Creation timestamp") - tags: List[str] = Field(default_factory=list, description="Tags associated with the registry") - credentials: Optional[str] = Field(None, description="Credentials reference for the registry") - - -class Registry(BaseModel): - """ - Represents a torero registry. - - A registry is a location where packages, modules, or artifacts can be stored and retrieved. - This could be an Ansible Galaxy registry, PyPI registry, or other package registries. - """ - name: str = Field(..., description="The name of the registry", examples=["ansible-galaxy-main", "pypi-internal"]) - description: Optional[str] = Field(None, description="Description of the registry") - type: str = Field(..., description="The type of registry (e.g., ansible-galaxy, pypi)", examples=["ansible-galaxy", "pypi"]) - url: str = Field(..., description="The URL of the registry", examples=["https://galaxy.ansible.com", "https://pypi.org/simple"]) - metadata: Optional[RegistryMetadata] = Field(None, description="Additional metadata for the registry") - - model_config = { - "json_schema_extra": { - "example": { - "name": "ansible-galaxy-main", - "description": "Main Ansible Galaxy registry", - "type": "ansible-galaxy", - "url": "https://galaxy.ansible.com", - "metadata": { - "id": "reg-123", - "created": "2024-01-01T00:00:00Z", - "tags": ["ansible", "production"], - "credentials": "galaxy-api-key" - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/repository.py b/opt/torero-api/torero_api/models/repository.py deleted file mode 100644 index 7319e50..0000000 --- a/opt/torero-api/torero_api/models/repository.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Repository model for torero API - -This module defines the Repository model, which represents a torero repository. -""" - -from typing import List, Optional, Dict, Any -from pydantic import BaseModel, Field - -class Repository(BaseModel): - """ - Represents a torero repository. - - Repositories are storage locations for torero components such as - services, decorators, and other resources. - - Attributes: - name: Unique identifier for the repository - description: Human-readable explanation of the repository's purpose - type: Category of repository (e.g., 'file', 'git', 's3') - location: Location URI for the repository - metadata: Optional additional metadata about the repository - """ - name: str = Field( - ..., - description="Unique identifier for the repository" - ) - description: Optional[str] = Field( - None, - description="Human-readable explanation of the repository's purpose" - ) - type: str = Field( - ..., - description="Category of repository (e.g., 'file', 'git', 's3')" - ) - location: str = Field( - ..., - description="Location URI for the repository" - ) - metadata: Optional[Dict[str, Any]] = Field( - None, - description="Additional metadata about the repository" - ) - - # Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "name": "local-services", - "description": "Local file repository for services", - "type": "file", - "location": "/etc/torero/services", - "metadata": { - "created": "2023-01-01T00:00:00Z", - "owner": "torero" - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/secret.py b/opt/torero-api/torero_api/models/secret.py deleted file mode 100644 index 8839d63..0000000 --- a/opt/torero-api/torero_api/models/secret.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Secret model for torero API - -This module defines the Secret model, which represents a torero secret. -""" - -from typing import List, Optional, Dict, Any -from pydantic import BaseModel, Field -from datetime import datetime - -class Secret(BaseModel): - """ - Represents a torero secret. - - Secrets are sensitive values stored securely and used by torero - services and decorators. - - Attributes: - name: Unique identifier for the secret - description: Human-readable explanation of the secret's purpose - type: Category of secret (e.g., 'password', 'api-key', 'token') - created_at: Timestamp when the secret was created - metadata: Optional additional metadata about the secret - """ - name: str = Field( - ..., - description="Unique identifier for the secret" - ) - description: Optional[str] = Field( - None, - description="Human-readable explanation of the secret's purpose" - ) - type: str = Field( - ..., - description="Category of secret (e.g., 'password', 'api-key', 'token')" - ) - created_at: Optional[datetime] = Field( - None, - description="Timestamp when the secret was created" - ) - metadata: Optional[Dict[str, Any]] = Field( - None, - description="Additional metadata about the secret" - ) - - # Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "name": "db-password", - "description": "Database password for the application", - "type": "password", - "created_at": "2023-01-01T00:00:00Z", - "metadata": { - "owner": "admin", - "provider": "vault" - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/models/service.py b/opt/torero-api/torero_api/models/service.py deleted file mode 100644 index 126f95c..0000000 --- a/opt/torero-api/torero_api/models/service.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Service model for torero API - -This module defines the Service model, which represents a torero service. -""" - -from typing import List, Optional, Literal -from pydantic import BaseModel, Field, field_validator - -# Literal type to enforce valid service types in query parameters -ServiceType = Literal["ansible-playbook", "opentofu-plan", "python-script"] - -class Service(BaseModel): - """ - Represents a torero service. - - A service is a runnable unit in torero that can be executed to perform specific tasks. - Services have different types (e.g., ansible-playbook, opentofu-plan, python-script) - and can be tagged for organization and filtering. - - Attributes: - name: Unique identifier for the service - description: Human-readable explanation of the service's purpose - type: Category of service indicating the underlying technology - tags: List of labels for grouping and filtering services - registries: Optional metadata about where the service is registered - """ - name: str = Field( - ..., - description="Unique identifier for the service" - ) - description: Optional[str] = Field( - None, - description="Human-readable explanation of the service's purpose" - ) - type: str = Field( - ..., - description="Category of service indicating the underlying technology" - ) - tags: List[str] = Field( - default_factory=list, - description="List of labels for grouping and filtering services" - ) - registries: Optional[dict] = Field( - None, - description="Metadata about where the service is registered" - ) - - @field_validator('tags', mode='before') - @classmethod - def validate_tags(cls, v): - """ - Validate and normalize the tags field. - - If tags is None or not provided, return an empty list. - If tags is already a list, return it as-is. - """ - if v is None: - return [] - if isinstance(v, list): - return v - # Handle any other edge cases - return [] - - # Updated Pydantic v2 configuration - model_config = { - "json_schema_extra": { - "example": { - "name": "network-backup", - "description": "Backs up configurations from network devices", - "type": "ansible-playbook", - "tags": ["network", "backup", "daily"], - "registries": { - "file": { - "path": "/etc/torero/services/network-backup" - } - } - } - } - } \ No newline at end of file diff --git a/opt/torero-api/torero_api/server.py b/opt/torero-api/torero_api/server.py deleted file mode 100644 index 22ede58..0000000 --- a/opt/torero-api/torero_api/server.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -Server module for torero-api - -This module provides functionality to start and configure the FastAPI server -that serves the torero API. -""" - -import logging -import os -import sys -import uvicorn -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import JSONResponse -from fastapi.openapi.utils import get_openapi - -from torero_api.api.v1.endpoints import services, decorators, repositories, secrets, execution, registries, database -from torero_api.models.common import APIInfo, ErrorResponse - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[ - logging.StreamHandler(sys.stdout), - ] -) -logger = logging.getLogger("torero-api") - -def create_app() -> FastAPI: - """ - Create and configure the FastAPI application instance. - - Returns: - FastAPI: Configured FastAPI application - """ - - # Create FastAPI application - app = FastAPI( - title="torero API", - version="0.1.0", - description=""" - RESTful API for interacting with torero services. - - This API provides endpoints for discovering and managing torero services, - allowing you to list services, filter by type or tag, and get metadata - about available service types and tags. - - Model Context Protocol (MCP) Integration: - This API follows OpenAPI standards and provides comprehensive type - information for seamless integration with MCP-enabled applications. - """ - ) - - - # Include the routers - app.include_router(services.router, prefix="/v1/services", tags=["services"]) - app.include_router(decorators.router, prefix="/v1/decorators", tags=["decorators"]) - app.include_router(repositories.router, prefix="/v1/repositories", tags=["repositories"]) - app.include_router(secrets.router, prefix="/v1/secrets", tags=["secrets"]) - app.include_router(registries.router, prefix="/v1/registries", tags=["registries"]) - app.include_router(execution.router, prefix="/v1/execute", tags=["execution"]) - app.include_router(database.router, prefix="/v1", tags=["database"]) - - # Root endpoint - @app.get("/", response_model=APIInfo, tags=["root"], - summary="API information", - description="Returns basic information about the API and available endpoints.") - async def root(): - """ - Return basic information about the API and available endpoints. - - This endpoint serves as an entry point to the API, providing metadata - about the API itself and links to the main endpoints following the - HATEOAS (Hypermedia as the Engine of Application State) principle. - """ - logger.debug("Root endpoint called") - api_port = os.environ.get("TORERO_API_PORT", "8000") - api_host = os.environ.get("TORERO_API_HOST", "localhost") - - return APIInfo( - name="torero API", - version="0.1.0", - description="API for torero services", - documentation=f"http://{api_host}:{api_port}/docs", - endpoints={ - "services": f"http://{api_host}:{api_port}/v1/services/", - "service_types": f"http://{api_host}:{api_port}/v1/services/types", - "service_tags": f"http://{api_host}:{api_port}/v1/services/tags", - "decorators": f"http://{api_host}:{api_port}/v1/decorators/", - "decorator_types": f"http://{api_host}:{api_port}/v1/decorators/types", - "repositories": f"http://{api_host}:{api_port}/v1/repositories/", - "repository_types": f"http://{api_host}:{api_port}/v1/repositories/types", - "secrets": f"http://{api_host}:{api_port}/v1/secrets/", - "secret_types": f"http://{api_host}:{api_port}/v1/secrets/types", - "registries": f"http://{api_host}:{api_port}/v1/registries/", - "registry_types": f"http://{api_host}:{api_port}/v1/registries/types", - "execute": f"http://{api_host}:{api_port}/v1/execute/", - "database_export": f"http://{api_host}:{api_port}/v1/db/export", - "database_import": f"http://{api_host}:{api_port}/v1/db/import", - } - ) - - # Health check endpoint - @app.get("/health", tags=["system"], - summary="API health check", - description="Check if the API is operational and can connect to torero.") - async def health_check(): - """ - Check if the API is operational and can connect to torero. - - This endpoint attempts to execute a simple torero command to verify - that the API can communicate with the torero CLI. - """ - from torero_api.core.torero_executor import check_torero_available - - try: - is_available, message = check_torero_available() - - if is_available: - return {"status": "healthy", "torero_available": True} - else: - logger.warning(f"torero health check failed: {message}") - return JSONResponse( - status_code=503, - content={ - "status": "unhealthy", - "torero_available": False, - "reason": message - } - ) - except Exception as e: - logger.error(f"Health check failed: {str(e)}") - return JSONResponse( - status_code=503, - content={ - "status": "unhealthy", - "torero_available": False, - "reason": f"Error checking torero: {str(e)}" - } - ) - - # Custom exception handler for consistent error responses - @app.exception_handler(HTTPException) - async def http_exception_handler(request: Request, exc: HTTPException): - """ - Custom exception handler for HTTPExceptions. - - Transforms HTTPExceptions into a consistent error response format. - """ - error = ErrorResponse( - status_code=exc.status_code, - detail=str(exc.detail), - error_type="http_error", - path=request.url.path - ) - return JSONResponse(status_code=exc.status_code, content=error.model_dump()) - - # Exception handler for unexpected errors - @app.exception_handler(Exception) - async def generic_exception_handler(request: Request, exc: Exception): - """ - Generic exception handler for unexpected errors. - - Catches any unhandled exceptions and returns a formatted error response. - """ - import traceback - logger.error(f"Unhandled exception: {str(exc)}") - logger.error(traceback.format_exc()) - - error = ErrorResponse( - status_code=500, - detail=f"Internal server error: {str(exc)}", - error_type="server_error", - path=request.url.path - ) - return JSONResponse(status_code=500, content=error.model_dump()) - - # Custom OpenAPI schema with enhanced metadata for MCP - def custom_openapi(): - """ - Generate a custom OpenAPI schema with enhanced metadata for MCP compatibility. - - This function extends the default OpenAPI schema with additional information - that helps MCP systems better understand the API structure and capabilities. - """ - if app.openapi_schema: - return app.openapi_schema - - openapi_schema = get_openapi( - title=app.title, - version=app.version, - description=app.description, - routes=app.routes, - ) - - # Add custom MCP-related metadata - openapi_schema["info"]["x-mcp-compatible"] = True - openapi_schema["info"]["x-mcp-version"] = "1.0" - openapi_schema["info"]["x-mcp-description"] = "This API is optimized for Model Context Protocol integration." - - # Add contact information - openapi_schema["info"]["contact"] = { - "name": "torero Development Team", - "url": "https://torero.dev/contact", - "email": "opensource@itential.com" - } - - # Add license information - openapi_schema["info"]["license"] = { - "name": "Apache License 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0" - } - - app.openapi_schema = openapi_schema - return app.openapi_schema - - # Set custom OpenAPI schema generator - app.openapi = custom_openapi - - return app - -def start_server(host: str = "0.0.0.0", port: int = 8000, log_level: str = "info", reload: bool = False): - """ - Start the FastAPI server using Uvicorn. - - Args: - host: The host to bind the server to - port: The port to bind the server to - log_level: The log level to use - reload: Whether to enable auto-reload - """ - logger.info(f"Starting torero API server on {host}:{port} with log level {log_level}") - - # Set environment variables for the API to use - os.environ["TORERO_API_PORT"] = str(port) - os.environ["TORERO_API_HOST"] = host - - try: - # Start the server - uvicorn.run( - "torero_api.server:app", - host=host, - port=port, - log_level=log_level, - reload=reload - ) - except Exception as e: - logger.error(f"Failed to start server: {str(e)}") - raise - -# Create the app instance -app = create_app() - -# Make sure these are exported at module level -__all__ = ["app", "create_app", "start_server"] \ No newline at end of file diff --git a/opt/torero-api/uv.lock b/opt/torero-api/uv.lock deleted file mode 100644 index c01ba72..0000000 --- a/opt/torero-api/uv.lock +++ /dev/null @@ -1,1839 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.10, <4.0" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.9.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'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, -] - -[[package]] -name = "babel" -version = "2.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, -] - -[[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 = "backrefs" -version = "5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, -] - -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, -] - -[[package]] -name = "build" -version = "1.2.2.post1" -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/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, -] - -[[package]] -name = "certifi" -version = "2025.6.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, -] - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, - { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, - { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, - { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, - { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, - { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, - { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, - { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, - { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, - { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "cryptography" -version = "45.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, - { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, - { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, - { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, - { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, - { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, - { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, - { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, - { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, - { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732, upload-time = "2025-06-10T00:03:27.896Z" }, - { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424, upload-time = "2025-06-10T00:03:29.992Z" }, - { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438, upload-time = "2025-06-10T00:03:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622, upload-time = "2025-06-10T00:03:33.491Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" }, - { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" }, - { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" }, - { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" }, -] - -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, -] - -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "fastapi" -version = "0.115.14" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, -] - -[[package]] -name = "filelock" -version = "3.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, -] - -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" }, - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "id" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, -] - -[[package]] -name = "identify" -version = "2.6.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -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.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, -] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, -] - -[[package]] -name = "jaraco-functools" -version = "4.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, -] - -[[package]] -name = "jeepney" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "keyring" -version = "25.6.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" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, -] - -[[package]] -name = "markdown" -version = "3.8.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.14" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fa/0101de32af88f87cf5cc23ad5f2e2030d00995f74e616306513431b8ab4b/mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754", size = 3951707, upload-time = "2025-05-13T13:27:57.173Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/a1/7fdb959ad592e013c01558822fd3c22931a95a0f08cf0a7c36da13a5b2b5/mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b", size = 8703767, upload-time = "2025-05-13T13:27:54.089Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "more-itertools" -version = "10.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, -] - -[[package]] -name = "mypy" -version = "1.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" }, - { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" }, - { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, - { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, - { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, -] - -[[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.2.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" }, - { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" }, - { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" }, - { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" }, - { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" }, - { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" }, - { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" }, - { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" }, - { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" }, - { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - -[[package]] -name = "pathspec" -version = "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.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, -] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -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" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { 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]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, -] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.1" -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/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } -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 = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - -[[package]] -name = "readme-renderer" -version = "44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "nh3" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - -[[package]] -name = "rfc3986" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, -] - -[[package]] -name = "rich" -version = "14.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "starlette" -version = "0.46.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, -] - -[[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 = "torero-api" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "fastapi" }, - { name = "pydantic" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[package.optional-dependencies] -all = [ - { name = "black" }, - { name = "flake8" }, - { name = "httpx" }, - { name = "isort" }, - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pyyaml" }, -] -dev = [ - { name = "black" }, - { name = "flake8" }, - { name = "httpx" }, - { name = "isort" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pyyaml" }, -] -docs = [ - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "pyyaml" }, -] -yaml = [ - { name = "pyyaml" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black" }, - { name = "build" }, - { name = "flake8" }, - { name = "httpx" }, - { name = "isort" }, - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pyyaml" }, - { name = "twine" }, -] - -[package.metadata] -requires-dist = [ - { name = "black", marker = "extra == 'dev'", specifier = ">=23.7.0" }, - { name = "fastapi", specifier = ">=0.115.0,<0.116.0" }, - { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.1.0" }, - { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.1" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, - { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.5.2" }, - { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.1.21" }, - { name = "pydantic", specifier = ">=2.0.0,<3.0.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "python-multipart", specifier = ">=0.0.20" }, - { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, - { name = "pyyaml", marker = "extra == 'docs'", specifier = ">=6.0" }, - { name = "pyyaml", marker = "extra == 'yaml'", specifier = ">=6.0" }, - { name = "torero-api", extras = ["yaml", "docs", "dev"], marker = "extra == 'all'" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.29.0,<0.30.0" }, -] -provides-extras = ["yaml", "docs", "dev", "all"] - -[package.metadata.requires-dev] -dev = [ - { name = "black", specifier = ">=23.7.0" }, - { name = "build", specifier = ">=0.10.0" }, - { name = "flake8", specifier = ">=6.1.0" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "isort", specifier = ">=5.12.0" }, - { name = "mypy", specifier = ">=1.5.0" }, - { name = "pre-commit", specifier = ">=3.3.3" }, - { name = "pytest", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", specifier = ">=0.21.0" }, - { name = "pytest-cov", specifier = ">=4.1.0" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "twine", specifier = ">=4.0.0" }, -] - -[[package]] -name = "twine" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "id" }, - { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, - { name = "packaging" }, - { name = "readme-renderer" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rfc3986" }, - { name = "rich" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404, upload-time = "2025-01-21T18:45:26.758Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.29.0" -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/49/8d/5005d39cd79c9ae87baf7d7aafdcdfe0b13aa69d9a1e3b7f1c984a2ac6d2/uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0", size = 40894, upload-time = "2024-03-20T06:43:25.747Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f5/cbb16fcbe277c1e0b8b3ddd188f2df0e0947f545c49119b589643632d156/uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", size = 60813, upload-time = "2024-03-20T06:43:21.841Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" }, - { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" }, - { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" }, - { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, -] - -[[package]] -name = "virtualenv" -version = "20.31.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/dd/579d1dc57f0f895426a1211c4ef3b0cb37eb9e642bb04bdcd962b5df206a/watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc", size = 405757, upload-time = "2025-06-15T19:04:51.058Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/7a0318cd874393344d48c34d53b3dd419466adf59a29ba5b51c88dd18b86/watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df", size = 397511, upload-time = "2025-06-15T19:04:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/06/be/503514656d0555ec2195f60d810eca29b938772e9bfb112d5cd5ad6f6a9e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68", size = 450739, upload-time = "2025-06-15T19:04:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0d/a05dd9e5f136cdc29751816d0890d084ab99f8c17b86f25697288ca09bc7/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc", size = 458106, upload-time = "2025-06-15T19:04:55.607Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fa/9cd16e4dfdb831072b7ac39e7bea986e52128526251038eb481effe9f48e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97", size = 484264, upload-time = "2025-06-15T19:04:57.009Z" }, - { url = "https://files.pythonhosted.org/packages/32/04/1da8a637c7e2b70e750a0308e9c8e662ada0cca46211fa9ef24a23937e0b/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c", size = 597612, upload-time = "2025-06-15T19:04:58.409Z" }, - { url = "https://files.pythonhosted.org/packages/30/01/109f2762e968d3e58c95731a206e5d7d2a7abaed4299dd8a94597250153c/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5", size = 477242, upload-time = "2025-06-15T19:04:59.786Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/46f58cf4969d3b7bc3ca35a98e739fa4085b0657a1540ccc29a1a0bc016f/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9", size = 453148, upload-time = "2025-06-15T19:05:01.103Z" }, - { url = "https://files.pythonhosted.org/packages/a5/cd/8267594263b1770f1eb76914940d7b2d03ee55eca212302329608208e061/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72", size = 626574, upload-time = "2025-06-15T19:05:02.582Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2f/7f2722e85899bed337cba715723e19185e288ef361360718973f891805be/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc", size = 624378, upload-time = "2025-06-15T19:05:03.719Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/64c88ec43d90a568234d021ab4b2a6f42a5230d772b987c3f9c00cc27b8b/watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587", size = 279829, upload-time = "2025-06-15T19:05:04.822Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/a9c1ed33de7af80935e4eac09570de679c6e21c07070aa99f74b4431f4d6/watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82", size = 292192, upload-time = "2025-06-15T19:05:06.348Z" }, - { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" }, - { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196, upload-time = "2025-06-15T19:05:11.91Z" }, - { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788, upload-time = "2025-06-15T19:05:13.373Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879, upload-time = "2025-06-15T19:05:14.725Z" }, - { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447, upload-time = "2025-06-15T19:05:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145, upload-time = "2025-06-15T19:05:17.17Z" }, - { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539, upload-time = "2025-06-15T19:05:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472, upload-time = "2025-06-15T19:05:19.588Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348, upload-time = "2025-06-15T19:05:20.856Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607, upload-time = "2025-06-15T19:05:21.937Z" }, - { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056, upload-time = "2025-06-15T19:05:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, - { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" }, - { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, - { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, - { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, - { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" }, - { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" }, - { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, - { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, - { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, - { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" }, - { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" }, - { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" }, - { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" }, - { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" }, - { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" }, - { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" }, - { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" }, - { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" }, - { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" }, - { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" }, - { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, - { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, - { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" }, - { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" }, - { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" }, - { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" }, - { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" }, - { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" }, - { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" }, - { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" }, - { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" }, - { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" }, - { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748, upload-time = "2025-06-15T19:06:44.2Z" }, - { url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801, upload-time = "2025-06-15T19:06:45.774Z" }, - { url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528, upload-time = "2025-06-15T19:06:46.791Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/98c7f4f7ce7ff03023cf971cd84a3ee3b790021ae7584ffffa0eb2554b96/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6", size = 454095, upload-time = "2025-06-15T19:06:48.211Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009, upload-time = "2025-06-15T19:06:52.896Z" }, -] - -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - -[[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" }, -] diff --git a/opt/torero-mcp/torero_mcp/client.py b/opt/torero-mcp/torero_mcp/client.py deleted file mode 100644 index cb559bd..0000000 --- a/opt/torero-mcp/torero_mcp/client.py +++ /dev/null @@ -1,558 +0,0 @@ -"""HTTP client for torero API.""" - -import json -import logging -from typing import Any, Dict, List, Optional, Union - -import httpx -from pydantic import BaseModel - -from .config import APIConfig, AuthConfig - -logger = logging.getLogger(__name__) - - -class ToreroAPIError(Exception): - """Base exception for torero API errors.""" - - def __init__(self, message: str, status_code: Optional[int] = None): - super().__init__(message) - self.status_code = status_code - - -class ToreroClient: - """HTTP client for torero API.""" - - def __init__(self, config: APIConfig): - """ - Initialize the torero client. - - Args: - config: API configuration - """ - self.config = config - self.base_url = config.base_url.rstrip("/") - - # Set up authentication headers - headers = {"Content-Type": "application/json"} - if config.auth: - if config.auth.type == "bearer" and config.auth.token: - headers["Authorization"] = f"Bearer {config.auth.token}" - elif config.auth.type == "basic" and config.auth.username and config.auth.password: - import base64 - credentials = base64.b64encode( - f"{config.auth.username}:{config.auth.password}".encode() - ).decode() - headers["Authorization"] = f"Basic {credentials}" - - # Create HTTP client - self._client = httpx.AsyncClient( - timeout=config.timeout, - verify=config.verify_ssl, - headers=headers, - follow_redirects=True, - ) - - async def __aenter__(self): - """Async context manager entry.""" - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async context manager exit.""" - await self._client.aclose() - - async def close(self) -> None: - """Close the HTTP client.""" - await self._client.aclose() - - async def _request( - self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - json_data: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: - """ - Make an HTTP request to the torero API. - - Args: - method: HTTP method - endpoint: API endpoint (relative to base URL) - params: Query parameters - json_data: JSON data for request body - - Returns: - Response data as dictionary - - Raises: - ToreroAPIError: If the request fails - """ - url = f"{self.base_url}{endpoint}" - - try: - logger.debug(f"Making {method} request to {url}") - if params: - logger.debug(f"Query parameters: {params}") - if json_data: - logger.debug(f"Request body: {json_data}") - - response = await self._client.request( - method=method, - url=url, - params=params, - json=json_data, - ) - - logger.debug(f"Response status: {response.status_code}") - - # Handle different response types - if response.status_code == 200: - return response.json() - elif response.status_code == 404: - raise ToreroAPIError("Resource not found", status_code=404) - elif response.status_code >= 400: - try: - error_data = response.json() - error_message = error_data.get("detail", f"API error: {response.status_code}") - except Exception: - error_message = f"API error: {response.status_code}" - raise ToreroAPIError(error_message, status_code=response.status_code) - else: - response.raise_for_status() - return response.json() - - except httpx.TimeoutException: - raise ToreroAPIError("Request timed out") - except httpx.RequestError as e: - raise ToreroAPIError(f"Request failed: {str(e)}") - except Exception as e: - if isinstance(e, ToreroAPIError): - raise - raise ToreroAPIError(f"Unexpected error: {str(e)}") - - async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """Make a GET request.""" - return await self._request("GET", endpoint, params=params) - - async def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """Make a POST request.""" - return await self._request("POST", endpoint, json_data=json_data) - - # Health check - async def health_check(self) -> Dict[str, Any]: - """Check API health.""" - return await self.get("/health") - - # Services - async def list_services( - self, - service_type: Optional[str] = None, - tag: Optional[str] = None, - skip: int = 0, - limit: int = 100 - ) -> List[Dict[str, Any]]: - """List services with optional filtering.""" - params = {"skip": skip, "limit": limit} - if service_type: - params["type"] = service_type - if tag: - params["tag"] = tag - - return await self.get("/v1/services/", params=params) - - async def get_service(self, name: str) -> Dict[str, Any]: - """Get a specific service by name.""" - return await self.get(f"/v1/services/{name}") - - async def describe_service(self, name: str) -> Dict[str, Any]: - """Get detailed description of a specific service.""" - return await self.get(f"/v1/services/{name}/describe") - - async def execute_service( - self, - service_name: str, - parameters: Optional[Dict[str, Any]] = None, - async_execution: bool = False, - timeout: Optional[int] = None - ) -> Dict[str, Any]: - """Execute a service with given parameters.""" - return await self.post( - f"/v1/services/{service_name}/execute", - json_data={ - "parameters": parameters or {}, - "async": async_execution, - "timeout": timeout - } - ) - - async def list_service_types(self) -> List[str]: - """List available service types.""" - return await self.get("/v1/services/types") - - async def list_service_tags(self) -> List[str]: - """List all service tags.""" - return await self.get("/v1/services/tags") - - # Service execution - async def execute_ansible_playbook(self, service_name: str) -> Dict[str, Any]: - """ - Execute an ansible-playbook service. - - Args: - service_name: Name of the ansible-playbook service - - Returns: - Execution result with return_code, stdout, stderr, timing info - """ - return await self.post(f"/v1/execute/ansible-playbook/{service_name}") - - async def execute_python_script(self, service_name: str) -> Dict[str, Any]: - """ - Execute a python-script service. - - Args: - service_name: Name of the python-script service - - Returns: - Execution result with return_code, stdout, stderr, timing info - """ - return await self.post(f"/v1/execute/python-script/{service_name}") - - async def execute_opentofu_plan_apply(self, service_name: str) -> Dict[str, Any]: - """ - Execute an OpenTofu plan service to apply infrastructure changes. - - Args: - service_name: Name of the OpenTofu plan service - - Returns: - Execution result with return_code, stdout, stderr, timing info - """ - return await self.post(f"/v1/execute/opentofu-plan/{service_name}/apply") - - async def execute_opentofu_plan_destroy(self, service_name: str) -> Dict[str, Any]: - """ - Execute an OpenTofu plan service to destroy infrastructure resources. - - Args: - service_name: Name of the OpenTofu plan service - - Returns: - Execution result with return_code, stdout, stderr, timing info - """ - return await self.post(f"/v1/execute/opentofu-plan/{service_name}/destroy") - - # Decorators - async def list_decorators( - self, - decorator_type: Optional[str] = None, - service_type: Optional[str] = None, - tag: Optional[str] = None, - skip: int = 0, - limit: int = 100 - ) -> List[Dict[str, Any]]: - """List decorators with optional filtering.""" - params = {"skip": skip, "limit": limit} - if decorator_type: - params["type"] = decorator_type - if service_type: - params["service_type"] = service_type - if tag: - params["tag"] = tag - - return await self.get("/v1/decorators/", params=params) - - async def get_decorator(self, name: str) -> Dict[str, Any]: - """Get a specific decorator by name.""" - return await self.get(f"/v1/decorators/{name}") - - async def list_decorator_types(self) -> List[str]: - """List available decorator types.""" - return await self.get("/v1/decorators/types") - - # Repositories - async def list_repositories( - self, - repo_type: Optional[str] = None, - tag: Optional[str] = None, - skip: int = 0, - limit: int = 100 - ) -> List[Dict[str, Any]]: - """List repositories with optional filtering.""" - params = {"skip": skip, "limit": limit} - if repo_type: - params["type"] = repo_type - if tag: - params["tag"] = tag - - return await self.get("/v1/repositories/", params=params) - - async def get_repository(self, name: str) -> Dict[str, Any]: - """Get a specific repository by name.""" - return await self.get(f"/v1/repositories/{name}") - - async def list_repository_types(self) -> List[str]: - """List available repository types.""" - return await self.get("/v1/repositories/types") - - # Secrets - async def list_secrets( - self, - secret_type: Optional[str] = None, - tag: Optional[str] = None, - skip: int = 0, - limit: int = 100 - ) -> List[Dict[str, Any]]: - """List secrets with optional filtering.""" - params = {"skip": skip, "limit": limit} - if secret_type: - params["type"] = secret_type - if tag: - params["tag"] = tag - - return await self.get("/v1/secrets/", params=params) - - async def get_secret(self, name: str, include_value: bool = False) -> Dict[str, Any]: - """Get a specific secret by name.""" - params = {"include_value": include_value} if include_value else {} - return await self.get(f"/v1/secrets/{name}", params=params) - - async def list_secret_types(self) -> List[str]: - """List available secret types.""" - return await self.get("/v1/secrets/types") - - # Executions - async def list_executions( - self, - service_name: Optional[str] = None, - status: Optional[str] = None, - skip: int = 0, - limit: int = 100 - ) -> List[Dict[str, Any]]: - """List executions with optional filtering.""" - params = {"skip": skip, "limit": limit} - if service_name: - params["service_name"] = service_name - if status: - params["status"] = status - - return await self.get("/v1/executions", params=params) - - async def get_execution_status(self, execution_id: str) -> Dict[str, Any]: - """Get execution status.""" - return await self.get(f"/v1/executions/{execution_id}") - - async def cancel_execution(self, execution_id: str) -> Dict[str, Any]: - """Cancel an execution.""" - return await self.post(f"/v1/executions/{execution_id}/cancel") - - async def stream_execution_logs(self, execution_id: str, follow: bool = True): - """Stream execution logs.""" - params = {"follow": follow} - - async with self._client.stream( - "GET", - f"{self.base_url}/v1/executions/{execution_id}/logs", - params=params - ) as response: - async for line in response.aiter_lines(): - if line.strip(): - yield json.loads(line) - - # System info - async def get_system_info(self) -> Dict[str, Any]: - """Get system information.""" - return await self.get("/v1/system/info") - - # Service descriptions - async def get_service_description(self, name: str) -> Dict[str, Any]: - """Get service description.""" - return await self.get(f"/v1/services/{name}/description") - - # Repository management - async def sync_repository(self, name: str, force: bool = False) -> Dict[str, Any]: - """Sync a repository.""" - return await self.post(f"/v1/repositories/{name}/sync", json_data={"force": force}) - - async def create_repository(self, name: str, url: str, **kwargs) -> Dict[str, Any]: - """Create a new repository.""" - data = {"name": name, "url": url, **kwargs} - return await self.post("/v1/repositories/", json_data=data) - - async def delete_repository(self, name: str) -> Dict[str, Any]: - """Delete a repository.""" - return await self.post(f"/v1/repositories/{name}/delete") - - # Secret management - async def create_secret(self, name: str, value: str, secret_type: str = "generic", **kwargs) -> Dict[str, Any]: - """Create a new secret.""" - data = {"name": name, "value": value, "type": secret_type, **kwargs} - return await self.post("/v1/secrets/", json_data=data) - - async def update_secret(self, name: str, value: Optional[str] = None, **kwargs) -> Dict[str, Any]: - """Update a secret.""" - data = {} - if value is not None: - data["value"] = value - data.update(kwargs) - return await self.post(f"/v1/secrets/{name}/update", json_data=data) - - async def delete_secret(self, name: str) -> Dict[str, Any]: - """Delete a secret.""" - return await self.post(f"/v1/secrets/{name}/delete") - - # Database import/export - async def export_database(self, format: str = "yaml") -> Dict[str, Any]: - """ - Export services and resources to a file. - - Args: - format: Export format (json or yaml, default: yaml) - - Returns: - Exported configuration data - """ - params = {"format": format} - return await self.get("/v1/db/export", params=params) - - async def export_database_download(self, format: str = "yaml", filename: Optional[str] = None) -> bytes: - """ - Export services and resources as a downloadable file. - - Args: - format: Export format (json or yaml, default: yaml) - filename: Optional filename for the export - - Returns: - File content as bytes - """ - params = {"format": format} - if filename: - params["filename"] = filename - - # Use raw response for file download - response = await self._client.get( - f"{self.base_url}/v1/db/export/download", - params=params - ) - response.raise_for_status() - return response.content - - async def import_database( - self, - file_content: str, - force: bool = False, - check: bool = False, - validate_only: bool = False - ) -> Dict[str, Any]: - """ - Import resources/services from a file. - - Args: - file_content: Content of the file to import - force: Force import even with conflicts - check: Check for conflicts before importing - validate_only: Only validate without importing - - Returns: - Import result with status and potential conflicts - """ - # Create multipart form data - files = {"file": ("import.yaml", file_content, "text/yaml")} - data = { - "force": str(force).lower(), - "check": str(check).lower(), - "validate_only": str(validate_only).lower() - } - - response = await self._client.post( - f"{self.base_url}/v1/db/import", - files=files, - data=data - ) - response.raise_for_status() - return response.json() - - async def check_database_import(self, file_content: str) -> Dict[str, Any]: - """ - Check what would happen during an import without actually importing. - - Args: - file_content: Content of the file to check - - Returns: - DatabaseImportCheckResult showing potential additions, replacements, conflicts - """ - files = {"file": ("check.yaml", file_content, "text/yaml")} - - response = await self._client.post( - f"{self.base_url}/v1/db/import/check", - files=files - ) - response.raise_for_status() - return response.json() - - async def import_database_from_repository( - self, - repository_url: str, - file_path: str, - branch: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - private_key_name: Optional[str] = None, - force: bool = False, - check: bool = False, - validate_only: bool = False - ) -> Dict[str, Any]: - """ - Import resources/services from a repository. - - Args: - repository_url: Repository URL - file_path: Path to the import file within the repository - branch: Optional branch name (sent as 'reference' to API) - username: Optional username for authentication - password: Optional password for authentication - private_key_name: Optional SSH private key name for authentication - force: Force import even with conflicts - check: Check for conflicts before importing - validate_only: Only validate without importing - - Returns: - Import result similar to standard import endpoint - """ - # Prepare form data according to API expectations - data = { - "repository": repository_url, # API expects 'repository' not 'repository_url' - "file_path": file_path, - "force": str(force).lower(), - "check": str(check).lower(), - "validate_only": str(validate_only).lower() - } - - if branch: - data["reference"] = branch # API expects 'reference' not 'branch' - if private_key_name: - data["private_key_name"] = private_key_name - if username: - data["username"] = username - if password: - data["password"] = password - - # Use form data (application/x-www-form-urlencoded) - response = await self._client.post( - f"{self.base_url}/v1/db/import/repository", - data=data, # This will use application/x-www-form-urlencoded - headers={"Content-Type": "application/x-www-form-urlencoded"} - ) - - # Better error handling for debugging - if response.status_code >= 400: - try: - error_data = response.json() - error_message = f"API error {response.status_code}: {error_data}" - except Exception: - error_message = f"API error {response.status_code}: {response.text}" - raise ToreroAPIError(error_message, status_code=response.status_code) - - return response.json() \ No newline at end of file From 96c8d423d8ddd68f59e582aa5520b46fe06b146f Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:02:22 -0400 Subject: [PATCH 02/13] Update docker compose defaults --- docker-compose.dev.yml | 3 --- docker-compose.yml | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 74fc31e..6da3d8e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,14 +8,11 @@ services: container_name: torero ports: - "22:22" - - "8000:8000" - "8001:8001" - "8080:8080" volumes: - ./data:/home/admin/data environment: - - ENABLE_API=true - - API_PORT=8000 - ENABLE_UI=true - UI_PORT=8001 - UI_REFRESH_INTERVAL=15 diff --git a/docker-compose.yml b/docker-compose.yml index bedd233..3b9ae47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,14 +5,11 @@ services: container_name: torero ports: - "22:22" - - "8000:8000" - "8001:8001" - "8080:8080" volumes: - ./data:/home/admin/data environment: - - ENABLE_API=true - - API_PORT=8000 - ENABLE_UI=true - UI_PORT=8001 - UI_REFRESH_INTERVAL=15 @@ -25,4 +22,4 @@ services: timeout: 10s retries: 3 start_period: 5s -... +... \ No newline at end of file From c0f54aaa126a3ed67b812befadfb4bffd086f49c Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:03:34 -0400 Subject: [PATCH 03/13] Remove legacy api supervisord config --- entrypoint.sh | 71 +++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 0733490..cca23a2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -314,23 +314,6 @@ serverurl=unix:///tmp/supervisor.sock EOF - # add torero-api service if enabled - if [[ "${ENABLE_API:-false}" == "true" ]]; then - local api_port="${API_PORT:-8000}" - cat >> /etc/supervisor/conf.d/torero-services.conf << EOF -[program:torero-api] -command=/usr/local/bin/torero-api --host 0.0.0.0 --port ${api_port} --log-file /home/admin/.torero-api.log -directory=/home/admin -user=admin -autostart=true -autorestart=true -redirect_stderr=true -stdout_logfile=/home/admin/.torero-api.log -stderr_logfile=/home/admin/.torero-api.log -environment=HOME="/home/admin",USER="admin" - -EOF - fi # add torero-mcp service if enabled if [[ "${ENABLE_MCP:-false}" == "true" ]]; then @@ -338,8 +321,7 @@ EOF local mcp_host="${TORERO_MCP_TRANSPORT_HOST:-0.0.0.0}" local mcp_port="${TORERO_MCP_TRANSPORT_PORT:-8080}" local mcp_path="${TORERO_MCP_TRANSPORT_PATH:-/sse}" - local api_base_url="${TORERO_API_BASE_URL:-http://localhost:${API_PORT:-8000}}" - local api_timeout="${TORERO_API_TIMEOUT:-30}" + local cli_timeout="${TORERO_CLI_TIMEOUT:-30}" local log_level="${TORERO_LOG_LEVEL:-INFO}" local mcp_log_file="${TORERO_MCP_LOG_FILE:-/home/admin/.torero-mcp.log}" @@ -353,7 +335,7 @@ autorestart=true redirect_stderr=true stdout_logfile=${mcp_log_file} stderr_logfile=${mcp_log_file} -environment=HOME="/home/admin",USER="admin",TORERO_MCP_TRANSPORT_TYPE="${mcp_transport}",TORERO_MCP_TRANSPORT_HOST="${mcp_host}",TORERO_MCP_TRANSPORT_PORT="${mcp_port}",TORERO_MCP_TRANSPORT_PATH="${mcp_path}",TORERO_API_BASE_URL="${api_base_url}",TORERO_API_TIMEOUT="${api_timeout}",TORERO_LOG_LEVEL="${log_level}" +environment=HOME="/home/admin",USER="admin",TORERO_MCP_TRANSPORT_TYPE="${mcp_transport}",TORERO_MCP_TRANSPORT_HOST="${mcp_host}",TORERO_MCP_TRANSPORT_PORT="${mcp_port}",TORERO_MCP_TRANSPORT_PATH="${mcp_path}",TORERO_CLI_TIMEOUT="${cli_timeout}",TORERO_LOG_LEVEL="${log_level}" EOF fi @@ -361,7 +343,6 @@ EOF # add torero-ui service if enabled if [[ "${ENABLE_UI:-false}" == "true" ]]; then local ui_port="${UI_PORT:-8001}" - local api_base_url="${TORERO_API_BASE_URL:-http://localhost:${API_PORT:-8000}}" local refresh_interval="${UI_REFRESH_INTERVAL:-30}" local ui_log_file="${TORERO_UI_LOG_FILE:-/home/admin/.torero-ui.log}" @@ -375,7 +356,18 @@ autorestart=true redirect_stderr=true stdout_logfile=${ui_log_file} stderr_logfile=${ui_log_file} -environment=HOME="/home/admin",USER="admin",DJANGO_SETTINGS_MODULE="torero_ui.settings",TORERO_API_BASE_URL="${api_base_url}",UI_REFRESH_INTERVAL="${refresh_interval}",DEBUG="False" +environment=HOME="/home/admin",USER="admin",DJANGO_SETTINGS_MODULE="torero_ui.settings",UI_REFRESH_INTERVAL="${refresh_interval}",DEBUG="False" + +[program:torero-ui-sync] +command=python torero_ui/manage.py sync_services --interval ${refresh_interval} +directory=/opt/torero-ui +user=admin +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=${ui_log_file} +stderr_logfile=${ui_log_file} +environment=HOME="/home/admin",USER="admin",DJANGO_SETTINGS_MODULE="torero_ui.settings",TORERO_CLI_TIMEOUT="30" EOF fi @@ -384,30 +376,6 @@ EOF return 0 } -setup_torero_api() { - if [[ "${ENABLE_API:-false}" != "true" ]]; then - echo "skipping torero-api setup as ENABLE_API is not set to true" - return 0 - fi - - local api_port="${API_PORT:-8000}" - echo "setting up torero-api runtime configuration..." - - # ensure db maps to admin user - if [ ! -d "/home/admin/.torero.d" ]; then - echo "creating torero database directory for admin user..." - mkdir -p /home/admin/.torero.d - chown -R admin:admin /home/admin/.torero.d - chmod 755 /home/admin/.torero.d - fi - - # create log file - touch /home/admin/.torero-api.log - chown admin:admin /home/admin/.torero-api.log - - echo "torero-api runtime setup completed (will be managed by supervisor)" - return 0 -} setup_torero_mcp() { if [[ "${ENABLE_MCP:-false}" != "true" ]]; then @@ -498,7 +466,6 @@ setup_torero_ui() { return 0 fi - local api_base_url="${TORERO_API_BASE_URL:-http://localhost:${API_PORT:-8000}}" local refresh_interval="${UI_REFRESH_INTERVAL:-30}" local ui_log_file="${TORERO_UI_LOG_FILE:-/home/admin/.torero-ui.log}" @@ -515,7 +482,6 @@ setup_torero_ui() { # run database migrations for runtime (static files collected at build time) echo "running database migrations for persistent storage..." su - admin -c "export DJANGO_SETTINGS_MODULE='torero_ui.settings' && \ - export TORERO_API_BASE_URL='${api_base_url}' && \ export UI_REFRESH_INTERVAL='${refresh_interval}' && \ export DEBUG='False' && \ cd /opt/torero-ui && \ @@ -532,15 +498,14 @@ unset CONTAINER_BUILD_MODE configure_dns setup_ssh_runtime handle_torero_eula -verify_opentofu || echo "OpenTofu verification failed, continuing without it" -setup_torero_api || echo "torero-api setup failed, continuing without it" +verify_opentofu || echo "opentofu verification failed, continuing without it" setup_torero_mcp || echo "torero-mcp setup failed, continuing without it" setup_torero_ui || echo "torero-ui setup failed, continuing without it" -setup_cli_capture || echo "CLI capture setup failed, continuing without it" +setup_cli_capture || echo "cli capture setup failed, continuing without it" setup_supervisor || echo "supervisor setup failed, continuing without it" -# Check if any services are enabled that need supervisor -if [[ "${ENABLE_API:-false}" == "true" || "${ENABLE_MCP:-false}" == "true" || "${ENABLE_UI:-false}" == "true" ]]; then +# check if any services are enabled that need supervisor +if [[ "${ENABLE_MCP:-false}" == "true" || "${ENABLE_UI:-false}" == "true" ]]; then echo "starting services with supervisor..." # Wait a moment for services to be ready sleep 2 From 90ef2aaa9fd80e7630ff16d8b16694c8a956e8a6 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:04:00 -0400 Subject: [PATCH 04/13] Update dependencies from consolidation --- opt/torero-mcp/pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/opt/torero-mcp/pyproject.toml b/opt/torero-mcp/pyproject.toml index 407b405..6de60b9 100644 --- a/opt/torero-mcp/pyproject.toml +++ b/opt/torero-mcp/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "torero-mcp" version = "0.1.0" -description = "MCP server for torero API integration" +description = "MCP server for direct torero CLI integration" authors = [ {name = "torero Development Team", email = "opensource@itential.com"} ] @@ -23,7 +23,6 @@ classifiers = [ ] dependencies = [ "fastmcp>=0.2.0", - "httpx>=0.25.0", "pydantic>=2.0.0", "pyyaml>=6.0", "click>=8.0.0", @@ -33,7 +32,6 @@ dependencies = [ dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", - "pytest-httpx>=0.22.0", "pytest-cov>=4.1.0", "black>=23.0.0", "isort>=5.12.0", From 8b2bfa64353ddce407101e8777d5667cd8746da1 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:08:46 -0400 Subject: [PATCH 05/13] Remove torero-api references --- opt/torero-mcp/torero_mcp/cli.py | 62 +++++++++++--------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/opt/torero-mcp/torero_mcp/cli.py b/opt/torero-mcp/torero_mcp/cli.py index 3a83ddf..7c9965a 100644 --- a/opt/torero-mcp/torero_mcp/cli.py +++ b/opt/torero-mcp/torero_mcp/cli.py @@ -37,11 +37,6 @@ def cli() -> None: type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), help="Override logging level" ) -@click.option( - "--api-url", - envvar="TORERO_API_BASE_URL", - help="torero API base URL (overrides config)" -) @click.option( "--transport", type=click.Choice(["stdio", "sse", "streamable_http"]), @@ -85,7 +80,6 @@ def cli() -> None: def run( config: Optional[Path] = None, log_level: Optional[str] = None, - api_url: Optional[str] = None, transport: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None, @@ -108,8 +102,6 @@ def run( # Override with CLI args if log_level: app_config.logging.level = log_level - if api_url: - app_config.api.base_url = api_url # Override transport settings if transport: @@ -210,7 +202,6 @@ def _run_server(app_config: Config) -> None: logger.info(f"Starting torero MCP server v{__version__}") logger.info(f"Configuration loaded from: {app_config}") - logger.debug(f"API URL: {app_config.api.base_url}") logger.debug(f"Log level: {app_config.logging.level}") logger.debug(f"Transport type: {app_config.mcp.transport.type}") if app_config.mcp.transport.type in ["sse", "streamable_http"]: @@ -243,13 +234,13 @@ def init_config(output: Path) -> None: config_content = """# torero MCP Server Configuration -api: - # Base URL for the torero API - base_url: "http://localhost:8000" - - # Request timeout in seconds +executor: + # CLI command timeout in seconds timeout: 30 + # torero command path (default: torero) + torero_command: "torero" + logging: # Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) level: "INFO" @@ -298,48 +289,37 @@ def init_config(output: Path) -> None: type=click.Path(exists=True, path_type=Path), help="Path to configuration file" ) -@click.option( - "--api-url", - envvar="TORERO_API_BASE_URL", - help="torero API base URL (overrides config)" -) def test_connection( config: Optional[Path] = None, - api_url: Optional[str] = None, ) -> None: - """Test connection to torero API.""" + """Test connection to torero CLI.""" async def _test() -> None: - from .client import ToreroClient, ToreroAPIError + from .executor import ToreroExecutor # Load config app_config = load_config(config) - # Override with CLI args - if api_url: - app_config.api.base_url = api_url - # Setup basic logging logging.basicConfig(level=logging.INFO) - click.echo(f"Testing connection to: {app_config.api.base_url}") + click.echo("Testing torero CLI connection...") try: - async with ToreroClient(app_config.api) as client: - health = await client.health_check() - click.echo("✓ Connection successful!") - click.echo(f"API Status: {health.get('status', 'unknown')}") - - # Test listing services - try: - services = await client.list_services(limit=1) - click.echo(f"✓ API functional - found {len(services)} service(s)") - except Exception as e: - click.echo(f"⚠ API connection OK but listing services failed: {e}") + executor = ToreroExecutor(timeout=app_config.executor.timeout) + + # Test version command + version = await executor.execute_command(["version"], parse_json=False) + click.echo("✓ Connection successful!") + click.echo(f"torero version: {version}") + + # Test listing services + try: + services = await executor.get_services() + click.echo(f"✓ CLI functional - found {len(services)} service(s)") + except Exception as e: + click.echo(f"⚠ CLI connection OK but listing services failed: {e}") - except ToreroAPIError as e: - click.echo(f"✗ API Error: {e}") - sys.exit(1) except Exception as e: click.echo(f"✗ Connection failed: {e}") sys.exit(1) From dbcc43013a73dc97a6d8a4c2347764576b1a14b6 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:10:36 -0400 Subject: [PATCH 06/13] Remove legacy `api` references --- opt/torero-mcp/torero_mcp/config.py | 136 ++++--- opt/torero-mcp/torero_mcp/server.py | 119 ++++--- .../torero_mcp/tools/database_tools.py | 333 +++--------------- .../torero_mcp/tools/decorator_tools.py | 105 +++--- .../torero_mcp/tools/execution_tools.py | 244 ++++--------- .../torero_mcp/tools/health_tools.py | 69 ++-- opt/torero-mcp/torero_mcp/tools/loader.py | 70 ++-- .../torero_mcp/tools/repository_tools.py | 186 ++++------ .../torero_mcp/tools/secret_tools.py | 205 ++++------- .../torero_mcp/tools/service_tools.py | 179 +++++----- opt/torero-ui/torero_ui/dashboard/services.py | 92 +++-- opt/torero-ui/torero_ui/settings.py | 5 +- 12 files changed, 662 insertions(+), 1081 deletions(-) diff --git a/opt/torero-mcp/torero_mcp/config.py b/opt/torero-mcp/torero_mcp/config.py index feeb799..67c1436 100644 --- a/opt/torero-mcp/torero_mcp/config.py +++ b/opt/torero-mcp/torero_mcp/config.py @@ -1,4 +1,4 @@ -"""Configuration management for torero MCP server.""" +"""configuration management for torero mcp server.""" import os import logging @@ -9,49 +9,20 @@ from pydantic import BaseModel, Field, field_validator -class AuthConfig(BaseModel): - """Authentication configuration.""" - - type: str = Field(..., description="Authentication type (bearer, basic)") - token: Optional[str] = Field(None, description="Bearer token") - username: Optional[str] = Field(None, description="Basic auth username") - password: Optional[str] = Field(None, description="Basic auth password") - - @field_validator("type") - @classmethod - def validate_auth_type(cls, v: str) -> str: - """Validate authentication type.""" - if v not in ["bearer", "basic"]: - raise ValueError("Auth type must be 'bearer' or 'basic'") - return v - - -class APIConfig(BaseModel): - """API configuration.""" - - base_url: str = Field( - default="http://localhost:8000", - description="Base URL for the torero API" - ) - timeout: int = Field(default=30, description="Request timeout in seconds") - verify_ssl: bool = Field(default=True, description="Verify SSL certificates") - auth: Optional[AuthConfig] = Field(None, description="Authentication config") - - class LoggingConfig(BaseModel): - """Logging configuration.""" + """Logging configuration for the MCP server.""" - level: str = Field(default="INFO", description="Logging level") + level: str = Field(default="INFO", description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)") format: str = Field( default="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - description="Log format" + description="Python logging format string" ) - file: Optional[str] = Field(None, description="Log file path") + file: Optional[str] = Field(None, description="Optional log file path for file-based logging") @field_validator("level") @classmethod def validate_log_level(cls, v: str) -> str: - """Validate logging level.""" + """Validate that the logging level is supported by Python's logging module.""" valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if v.upper() not in valid_levels: raise ValueError(f"Log level must be one of: {valid_levels}") @@ -59,64 +30,81 @@ def validate_log_level(cls, v: str) -> str: class TransportConfig(BaseModel): - """Transport configuration for MCP server.""" + """Transport configuration for the MCP server communication layer.""" - type: str = Field(default="stdio", description="Transport type: stdio, sse, or streamable_http") - host: str = Field(default="127.0.0.1", description="Host for SSE/HTTP transport") - port: int = Field(default=8000, description="Port for SSE/HTTP transport") - path: str = Field(default="/sse", description="SSE endpoint path") + type: str = Field( + default="stdio", + description="Transport protocol: stdio for direct process communication, sse for Server-Sent Events, or streamable_http for HTTP streaming" + ) + host: str = Field(default="127.0.0.1", description="Host address for network-based transports (sse, streamable_http)") + port: int = Field(default=8000, description="Network port for network-based transports (sse, streamable_http)") + path: str = Field(default="/sse", description="URL path for Server-Sent Events endpoint") @field_validator("type") @classmethod def validate_transport_type(cls, v: str) -> str: - """Validate transport type.""" + """Validate that the transport type is one of the supported MCP transport protocols.""" if v not in ["stdio", "sse", "streamable_http"]: raise ValueError("Transport type must be 'stdio', 'sse', or 'streamable_http'") return v +class ExecutorConfig(BaseModel): + """Configuration for the torero CLI executor.""" + + timeout: int = Field(default=30, description="Default timeout in seconds for torero CLI commands") + torero_command: str = Field(default="torero", description="Path or name of the torero CLI executable") + + class MCPConfig(BaseModel): - """MCP server configuration.""" + """Core MCP server configuration.""" - name: str = Field(default="torero", description="Server name") - version: str = Field(default="0.1.0", description="Server version") - transport: TransportConfig = Field(default_factory=TransportConfig, description="Transport configuration") + name: str = Field(default="torero", description="Name identifier for this MCP server instance") + version: str = Field(default="0.1.0", description="Version of this MCP server implementation") + transport: TransportConfig = Field(default_factory=TransportConfig, description="Transport layer configuration") class Config(BaseModel): - """Main configuration.""" + """Main configuration object containing all server settings.""" - api: APIConfig = Field(default_factory=APIConfig) - logging: LoggingConfig = Field(default_factory=LoggingConfig) - mcp: MCPConfig = Field(default_factory=MCPConfig) + logging: LoggingConfig = Field(default_factory=LoggingConfig, description="Logging configuration") + mcp: MCPConfig = Field(default_factory=MCPConfig, description="MCP server configuration") + executor: ExecutorConfig = Field(default_factory=ExecutorConfig, description="torero CLI executor configuration") def load_config(config_path: Optional[Union[str, Path]] = None) -> Config: """ - Load configuration from file and environment variables. + Load configuration from YAML file and environment variables. + + Environment variables take precedence over file configuration. + The configuration supports nested structures for organizing related settings. Args: - config_path: Path to configuration file + config_path: Optional path to a YAML configuration file. If None, only environment variables are used. Returns: - Config: Loaded configuration + Config: A validated configuration object with all settings loaded and validated. + + Environment Variables: + - TORERO_LOG_LEVEL: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + - TORERO_LOG_FILE: Path to log file (optional) + - TORERO_MCP_TRANSPORT_TYPE: Transport type (stdio, sse, streamable_http) + - TORERO_MCP_TRANSPORT_HOST: Host for network transports + - TORERO_MCP_TRANSPORT_PORT: Port for network transports + - TORERO_MCP_TRANSPORT_PATH: Path for SSE endpoint + - TORERO_CLI_TIMEOUT: Timeout for torero CLI commands in seconds """ config_data: Dict[str, Any] = {} - # Load from file if provided + # load from yaml file if provided if config_path: config_file = Path(config_path) if config_file.exists(): with open(config_file, "r", encoding="utf-8") as f: config_data = yaml.safe_load(f) or {} - # Override with environment variables + # override with environment variables env_overrides = { - "api": { - "base_url": os.getenv("TORERO_API_BASE_URL"), - "timeout": os.getenv("TORERO_API_TIMEOUT"), - "verify_ssl": os.getenv("TORERO_API_VERIFY_SSL"), - }, "logging": { "level": os.getenv("TORERO_LOG_LEVEL"), "file": os.getenv("TORERO_LOG_FILE"), @@ -128,10 +116,14 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config: "port": os.getenv("TORERO_MCP_TRANSPORT_PORT"), "path": os.getenv("TORERO_MCP_TRANSPORT_PATH"), } + }, + "executor": { + "timeout": os.getenv("TORERO_CLI_TIMEOUT"), + "torero_command": os.getenv("TORERO_CLI_COMMAND"), } } - # Merge environment overrides + # merge environment overrides into config data for section, values in env_overrides.items(): if section not in config_data: config_data[section] = {} @@ -139,21 +131,19 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config: if isinstance(values, dict): for key, value in values.items(): if isinstance(value, dict): - # Handle nested config like transport + # handle nested config like transport if key not in config_data[section]: config_data[section][key] = {} for sub_key, sub_value in value.items(): if sub_value is not None: - # Convert string values to appropriate types - if sub_key == "port": + # convert string values to appropriate types + if sub_key == "port" or sub_key == "timeout": sub_value = int(sub_value) config_data[section][key][sub_key] = sub_value elif value is not None: - # Convert string values to appropriate types + # convert string values to appropriate types if key == "timeout": value = int(value) - elif key == "verify_ssl": - value = value.lower() in ("true", "1", "yes", "on") config_data[section][key] = value return Config(**config_data) @@ -161,12 +151,15 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config: def setup_logging(config: LoggingConfig) -> None: """ - Set up logging based on configuration. + Configure Python logging based on the provided logging configuration. + + Sets up console and/or file logging with the specified format and level. + Also configures third-party library logging to reduce noise. Args: - config: Logging configuration + config: LoggingConfig object containing logging preferences including level, format, and optional file output. """ - # Configure logging + # configure main logging log_config = { "level": getattr(logging, config.level), "format": config.format, @@ -178,5 +171,6 @@ def setup_logging(config: LoggingConfig) -> None: logging.basicConfig(**log_config) - # Set httpx logging to WARNING to reduce noise - logging.getLogger("httpx").setLevel(logging.WARNING) \ No newline at end of file + # reduce noise from third-party libraries + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.WARNING) \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/server.py b/opt/torero-mcp/torero_mcp/server.py index b14e2b8..12d47a3 100644 --- a/opt/torero-mcp/torero_mcp/server.py +++ b/opt/torero-mcp/torero_mcp/server.py @@ -1,4 +1,4 @@ -"""MCP server implementation for torero.""" +"""mcp server implementation for torero.""" import logging from typing import Any, Dict, List, Optional, Sequence @@ -6,7 +6,7 @@ from fastmcp import FastMCP from pydantic import BaseModel, Field -from .client import ToreroClient, ToreroAPIError +from .executor import ToreroExecutor, ToreroExecutorError from .config import Config from .tools.loader import ToolLoader @@ -14,81 +14,81 @@ class ToreroMCPServer: - """MCP server for torero API integration.""" + """mcp server for torero direct cli integration.""" def __init__(self, config: Config): """ - Initialize the torero MCP server. + initialize the torero mcp server. - Args: - config: Server configuration + args: + config: server configuration """ self.config = config - self.client = ToreroClient(config.api) + self.executor = ToreroExecutor(timeout=config.executor.timeout) self.mcp = FastMCP(config.mcp.name) - self.tool_loader = ToolLoader(self.client) + self.tool_loader = ToolLoader(self.executor) self._setup_tools() def _setup_tools(self) -> None: - """Set up MCP tools dynamically.""" + """set up mcp tools dynamically.""" import inspect from functools import wraps - # Load all tools from the tools directory + # load all tools from the tools directory tools = self.tool_loader.load_all_tools() - # Register each tool with FastMCP + # register each tool with fastmcp registered_count = 0 for tool_name, tool_func in tools.items(): try: - # Get the function signature + # get the function signature sig = inspect.signature(tool_func) params = list(sig.parameters.values()) - # Skip the client parameter - if params and params[0].name == 'client': + # skip the executor parameter + if params and params[0].name == 'executor': params = params[1:] - # For functions with no parameters (except client), create a simple wrapper + # for functions with no parameters (except executor), create a simple wrapper if len(params) == 0: - # Create a factory function that returns a wrapper - def make_wrapper(func, client): + # create a factory function that returns a wrapper + def make_wrapper(func, executor): async def wrapper(): - return await func(client) + return await func(executor) wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ return wrapper - wrapper = make_wrapper(tool_func, self.client) + wrapper = make_wrapper(tool_func, self.executor) else: - # For functions with parameters, use the existing wrapper creation + # for functions with parameters, use the existing wrapper creation wrapper = self._create_tool_wrapper(tool_func) - # Register the wrapper with FastMCP + # register the wrapper with fastmcp decorated_tool = self.mcp.tool()(wrapper) - logger.debug(f"Registered tool: {tool_name}") + logger.debug(f"registered tool: {tool_name}") registered_count += 1 except Exception as e: - logger.error(f"Failed to register tool {tool_name}: {e}") + logger.error(f"failed to register tool {tool_name}: {e}") - logger.info(f"Successfully registered {registered_count} tools") + logger.info(f"successfully registered {registered_count} tools") def _create_tool_wrapper(self, tool_func): - """Create a wrapper function that injects the client parameter.""" + """create a wrapper function that injects the executor parameter.""" import inspect from functools import wraps - # Get function signature and parameters + # get function signature and parameters sig = inspect.signature(tool_func) params = list(sig.parameters.values()) - # Skip the client parameter - if params and params[0].name == 'client': + # skip the executor parameter + if params and params[0].name == 'executor': params = params[1:] - # Create a dynamic wrapper that preserves parameter names + # create a dynamic wrapper that preserves parameter names def create_wrapper(): - # Build parameter names and defaults + # build parameter names and defaults param_names = [p.name for p in params] param_defaults = {} @@ -96,18 +96,18 @@ def create_wrapper(): if p.default != inspect.Parameter.empty: param_defaults[p.name] = p.default - # Create wrapper function dynamically + # create wrapper function dynamically @wraps(tool_func) async def wrapper(**kwargs): - # Fill missing parameters with defaults + # fill missing parameters with defaults for name in param_names: if name not in kwargs and name in param_defaults: kwargs[name] = param_defaults[name] - # Call original function with client and provided kwargs - return await tool_func(self.client, **kwargs) + # call original function with executor and provided kwargs + return await tool_func(self.executor, **kwargs) - # Preserve original signature (without client parameter) + # preserve original signature (without executor parameter) wrapper.__signature__ = inspect.Signature(params) wrapper.__name__ = tool_func.__name__ wrapper.__doc__ = tool_func.__doc__ @@ -117,34 +117,38 @@ async def wrapper(**kwargs): return create_wrapper() async def test_connection(self) -> None: - """Test API connection.""" + """test torero cli connection.""" try: - await self.client.health_check() - logger.info("Successfully connected to torero API") + is_available, message = self.executor.check_torero_available() + if is_available: + logger.info("successfully connected to torero cli") + else: + logger.warning(f"torero cli not available: {message}") + logger.info("server will start anyway, but tools may fail") except Exception as e: - logger.warning(f"Could not connect to torero API: {e}") - logger.info("Server will start anyway, but tools may fail") + logger.warning(f"could not test torero cli: {e}") + logger.info("server will start anyway, but tools may fail") def run(self) -> None: - """Run the MCP server.""" - logger.info(f"Starting torero MCP server '{self.config.mcp.name}'") - logger.info(f"Connecting to torero API at: {self.config.api.base_url}") + """run the mcp server.""" + logger.info(f"starting torero mcp server '{self.config.mcp.name}'") + logger.info("using direct torero cli integration") transport_config = self.config.mcp.transport - logger.info(f"Using {transport_config.type} transport") + logger.info(f"using {transport_config.type} transport") - # Add ready signal for AnythingLLM - logger.info("🚀 torero MCP server is ready for connections") - # Only print to stdout for non-stdio transports to avoid interfering with JSON-RPC protocol + # add ready signal for anythingllm + logger.info("🚀 torero mcp server is ready for connections") + # only print to stdout for non-stdio transports to avoid interfering with json-rpc protocol if transport_config.type != "stdio": - print("🚀 torero MCP server is ready for connections", flush=True) + print("🚀 torero mcp server is ready for connections", flush=True) - # Let FastMCP handle the event loop with appropriate transport + # let fastmcp handle the event loop with appropriate transport if transport_config.type == "stdio": self.mcp.run() elif transport_config.type == "sse": - logger.info(f"Starting SSE server on {transport_config.host}:{transport_config.port}") - logger.info(f"SSE endpoint: {transport_config.path}") + logger.info(f"starting sse server on {transport_config.host}:{transport_config.port}") + logger.info(f"sse endpoint: {transport_config.path}") self.mcp.run( transport="sse", host=transport_config.host, @@ -152,15 +156,20 @@ def run(self) -> None: path=transport_config.path ) elif transport_config.type == "streamable_http": - logger.info(f"Starting Streamable HTTP server on {transport_config.host}:{transport_config.port}") + logger.info(f"starting streamable http server on {transport_config.host}:{transport_config.port}") self.mcp.run( transport="streamable_http", host=transport_config.host, port=transport_config.port ) else: - raise ValueError(f"Unknown transport type: {transport_config.type}") + raise ValueError(f"unknown transport type: {transport_config.type}") async def close(self) -> None: - """Close the server and cleanup resources.""" - await self.client.close() \ No newline at end of file + """Close the server and cleanup resources. + + Performs cleanup operations when shutting down the MCP server. + This method is primarily used for resource cleanup and graceful shutdown. + """ + # no cleanup needed for direct cli integration + logger.info("mcp server shutdown complete") \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/database_tools.py b/opt/torero-mcp/torero_mcp/tools/database_tools.py index 77942aa..a600fd0 100644 --- a/opt/torero-mcp/torero_mcp/tools/database_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/database_tools.py @@ -1,47 +1,46 @@ -"""Database import/export tools for torero MCP server.""" +"""database import/export tools for torero mcp server.""" import json import logging -from pathlib import Path from typing import Optional -from ..client import ToreroClient, ToreroAPIError +from ..executor import ToreroExecutor, ToreroExecutorError logger = logging.getLogger(__name__) async def export_database( - client: ToreroClient, + executor: ToreroExecutor, format: str = "yaml" ) -> str: - """Export torero database configuration. + """export torero database configuration. - Exports all services, repositories, decorators, and secrets metadata - to a YAML or JSON format that can be used for backup or migration. + exports all services, repositories, decorators, and secrets metadata + to a yaml or json format that can be used for backup or migration. - Args: - client: ToreroClient instance - format: Export format - either "yaml" or "json" (default: "yaml") + args: + executor: toreroexecutor instance + format: export format - either "yaml" or "json" (default: "yaml") - Returns: - JSON string containing the exported configuration data + returns: + json string containing the exported configuration data - Examples: - Export to YAML format: + examples: + export to yaml format: >>> export_database(format="yaml") - Export to JSON format: + export to json format: >>> export_database(format="json") """ try: if format not in ["yaml", "json"]: return json.dumps({ - "error": "Invalid format. Must be 'yaml' or 'json'", + "error": "invalid format. must be 'yaml' or 'json'", "supported_formats": ["yaml", "json"] }, indent=2) - logger.info(f"Exporting database in {format} format") - result = await client.export_database(format=format) + logger.info(f"exporting database in {format} format") + result = await executor.export_database(format=format) return json.dumps({ "status": "success", @@ -49,277 +48,50 @@ async def export_database( "data": result }, indent=2) - except ToreroAPIError as e: - logger.error(f"API error exporting database: {e}") + except ToreroExecutorError as e: + logger.error(f"executor error exporting database: {e}") return json.dumps({ - "error": f"Failed to export database: {e}", - "status_code": e.status_code + "error": f"failed to export database: {e}" }, indent=2) except Exception as e: - logger.exception("Unexpected error exporting database") + logger.exception("unexpected error exporting database") return json.dumps({ - "error": f"Unexpected error: {e}" - }, indent=2) - - -async def export_database_to_file( - client: ToreroClient, - filename: str, - format: str = "yaml" -) -> str: - """Export torero database configuration to a file. - - Exports all services, repositories, decorators, and secrets metadata - to a file in YAML or JSON format. - - Args: - client: ToreroClient instance - filename: Path where the export file will be saved - format: Export format - either "yaml" or "json" (default: "yaml") - - Returns: - JSON string with export status and file location - - Examples: - Export to YAML file: - >>> export_database_to_file(filename="backup.yaml", format="yaml") - - Export to JSON file: - >>> export_database_to_file(filename="backup.json", format="json") - """ - try: - if format not in ["yaml", "json"]: - return json.dumps({ - "error": "Invalid format. Must be 'yaml' or 'json'", - "supported_formats": ["yaml", "json"] - }, indent=2) - - logger.info(f"Exporting database to file: {filename}") - content = await client.export_database_download(format=format, filename=filename) - - # Save to file - output_path = Path(filename) - output_path.write_bytes(content) - - return json.dumps({ - "status": "success", - "format": format, - "filename": str(output_path.absolute()), - "size_bytes": len(content) - }, indent=2) - - except ToreroAPIError as e: - logger.error(f"API error exporting database to file: {e}") - return json.dumps({ - "error": f"Failed to export database: {e}", - "status_code": e.status_code - }, indent=2) - except Exception as e: - logger.exception("Unexpected error exporting database to file") - return json.dumps({ - "error": f"Unexpected error: {e}" + "error": f"unexpected error: {e}" }, indent=2) async def import_database( - client: ToreroClient, - filename: str, - force: bool = False, - check: bool = False, - validate_only: bool = False -) -> str: - """Import torero database configuration from a file. - - Imports services, repositories, decorators, and secrets from a YAML or JSON file. - Supports dry-run validation and conflict resolution. - - Args: - client: ToreroClient instance - filename: Path to the import file - force: Force import even with conflicts (default: False) - check: Check for conflicts before importing (default: False) - validate_only: Only validate without importing (default: False) - - Returns: - JSON string with import result and any conflicts - - Examples: - Basic import: - >>> import_database(filename="config.yaml") - - Check before import: - >>> import_database(filename="config.yaml", check=True) - - Force import with conflicts: - >>> import_database(filename="config.yaml", force=True) - - Validate only: - >>> import_database(filename="config.yaml", validate_only=True) - """ - try: - import_path = Path(filename) - if not import_path.exists(): - return json.dumps({ - "error": f"File not found: {filename}" - }, indent=2) - - # Read file content - file_content = import_path.read_text() - - logger.info(f"Importing database from file: {filename}") - result = await client.import_database( - file_content=file_content, - force=force, - check=check, - validate_only=validate_only - ) - - return json.dumps({ - "status": "success", - "filename": str(import_path.absolute()), - "result": result, - "options": { - "force": force, - "check": check, - "validate_only": validate_only - } - }, indent=2) - - except ToreroAPIError as e: - logger.error(f"API error importing database: {e}") - return json.dumps({ - "error": f"Failed to import database: {e}", - "status_code": e.status_code - }, indent=2) - except Exception as e: - logger.exception("Unexpected error importing database") - return json.dumps({ - "error": f"Unexpected error: {e}" - }, indent=2) - - -async def check_database_import( - client: ToreroClient, - filename: str -) -> str: - """Check what would happen during a database import. - - Analyzes the import file and reports what resources would be added, - replaced, or cause conflicts, without actually performing the import. - - Args: - client: ToreroClient instance - filename: Path to the import file to check - - Returns: - JSON string with analysis of potential additions, replacements, and conflicts - - Examples: - Check import file: - >>> check_database_import(filename="config.yaml") - """ - try: - import_path = Path(filename) - if not import_path.exists(): - return json.dumps({ - "error": f"File not found: {filename}" - }, indent=2) - - # Read file content - file_content = import_path.read_text() - - logger.info(f"Checking database import from file: {filename}") - result = await client.check_database_import(file_content=file_content) - - return json.dumps({ - "status": "success", - "filename": str(import_path.absolute()), - "check_result": result - }, indent=2) - - except ToreroAPIError as e: - logger.error(f"API error checking database import: {e}") - return json.dumps({ - "error": f"Failed to check import: {e}", - "status_code": e.status_code - }, indent=2) - except Exception as e: - logger.exception("Unexpected error checking database import") - return json.dumps({ - "error": f"Unexpected error: {e}" - }, indent=2) - - -async def import_database_from_repository( - client: ToreroClient, - repository_url: str, + executor: ToreroExecutor, file_path: str, - branch: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - private_key_name: Optional[str] = None, + repository: Optional[str] = None, + reference: Optional[str] = None, + private_key: Optional[str] = None, force: bool = False, check: bool = False, validate_only: bool = False ) -> str: - """Import torero database configuration from a git repository. - - Imports services, repositories, decorators, and secrets from a file - in a git repository. Supports authentication and conflict resolution. + """import torero database configuration from a file or repository. - Args: - client: ToreroClient instance - repository_url: Git repository URL - file_path: Path to the import file within the repository - branch: Optional branch name (default: repository default branch) - username: Optional username for repository authentication - password: Optional password for repository authentication - private_key_name: Optional SSH private key name for authentication - force: Force import even with conflicts (default: False) - check: Check for conflicts before importing (default: False) - validate_only: Only validate without importing (default: False) - - Returns: - JSON string with import result and any conflicts - - Examples: - Import from public repository: - >>> import_database_from_repository( - ... repository_url="https://github.com/org/config", - ... file_path="torero/config.yaml" - ... ) - - Import from private repository with HTTP auth: - >>> import_database_from_repository( - ... repository_url="https://github.com/org/private-config", - ... file_path="torero/config.yaml", - ... username="user", - ... password="token" - ... ) - - Import from private repository with SSH key: - >>> import_database_from_repository( - ... repository_url="git@github.com:org/private-config.git", - ... file_path="torero/config.yaml", - ... private_key_name="my-ssh-key" - ... ) - - Import from specific branch: - >>> import_database_from_repository( - ... repository_url="https://github.com/org/config", - ... file_path="torero/config.yaml", - ... branch="develop" - ... ) + args: + executor: toreroexecutor instance + file_path: path to the import file + repository: optional repository url + reference: optional branch/reference + private_key: optional ssh private key name + force: force import even with conflicts + check: check for conflicts before importing + validate_only: only validate without importing + + returns: + json string with import result """ try: - logger.info(f"Importing database from repository: {repository_url}") - result = await client.import_database_from_repository( - repository_url=repository_url, + logger.info(f"importing database from: {file_path}") + result = await executor.import_database( file_path=file_path, - branch=branch, - username=username, - password=password, - private_key_name=private_key_name, + repository=repository, + reference=reference, + private_key=private_key, force=force, check=check, validate_only=validate_only @@ -327,27 +99,22 @@ async def import_database_from_repository( return json.dumps({ "status": "success", - "repository_url": repository_url, "file_path": file_path, - "branch": branch or "default", "result": result, "options": { "force": force, "check": check, - "validate_only": validate_only, - "authenticated": bool(username or private_key_name), - "auth_type": "ssh" if private_key_name else ("http" if username else "none") + "validate_only": validate_only } }, indent=2) - except ToreroAPIError as e: - logger.error(f"API error importing from repository: {e}") + except ToreroExecutorError as e: + logger.error(f"executor error importing database: {e}") return json.dumps({ - "error": f"Failed to import from repository: {e}", - "status_code": e.status_code + "error": f"failed to import database: {e}" }, indent=2) except Exception as e: - logger.exception("Unexpected error importing from repository") + logger.exception("unexpected error importing database") return json.dumps({ - "error": f"Unexpected error: {e}" + "error": f"unexpected error: {e}" }, indent=2) \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/decorator_tools.py b/opt/torero-mcp/torero_mcp/tools/decorator_tools.py index e3cef5b..441e365 100644 --- a/opt/torero-mcp/torero_mcp/tools/decorator_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/decorator_tools.py @@ -1,85 +1,98 @@ -"""Decorator-related tools for torero MCP server.""" +"""decorator-related tools for torero mcp server.""" import json import logging from typing import Any, Dict, Optional -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) async def list_decorators( - client: ToreroClient, + executor: ToreroExecutor, decorator_type: Optional[str] = None, service_type: Optional[str] = None, tag: Optional[str] = None, limit: int = 100 ) -> str: """ - List torero decorators with optional filtering. + list torero decorators with optional filtering. - Args: - client: ToreroClient instance - decorator_type: Filter by decorator type (e.g., 'authentication', 'logging') - service_type: Filter by applicable service type - tag: Filter by tag - limit: Maximum number of decorators to return (default: 100) + args: + executor: toreroexecutor instance + decorator_type: filter by decorator type (e.g., 'authentication', 'logging') + service_type: filter by applicable service type + tag: filter by tag + limit: maximum number of decorators to return (default: 100) - Returns: - JSON string containing list of decorators + returns: + json string containing list of decorators """ try: - decorators = await client.list_decorators( - decorator_type=decorator_type, - service_type=service_type, - tag=tag, - limit=limit - ) + decorators = await executor.get_decorators() + + # apply filters + if decorator_type: + decorators = [d for d in decorators if d.get('type') == decorator_type] + if service_type: + decorators = [d for d in decorators if service_type in d.get('service_types', [])] + if tag: + decorators = [d for d in decorators if tag in d.get('tags', [])] + + # apply limit + decorators = decorators[:limit] + return json.dumps(decorators, indent=2) - except ToreroAPIError as e: - return f"Error listing decorators: {e}" + except ToreroExecutorError as e: + return f"error listing decorators: {e}" except Exception as e: - logger.exception("Unexpected error in list_decorators") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_decorators") + return f"unexpected error: {e}" -async def get_decorator(client: ToreroClient, name: str) -> str: +async def get_decorator(executor: ToreroExecutor, name: str) -> str: """ - Get detailed information about a specific torero decorator. + get detailed information about a specific torero decorator. - Args: - client: ToreroClient instance - name: Name of the decorator to retrieve + args: + executor: toreroexecutor instance + name: name of the decorator to retrieve - Returns: - JSON string containing decorator details + returns: + json string containing decorator details """ try: - decorator = await client.get_decorator(name) - return json.dumps(decorator, indent=2) - except ToreroAPIError as e: - return f"Error getting decorator '{name}': {e}" + decorators = await executor.get_decorators() + decorator = next((d for d in decorators if d.get('name') == name), None) + + if decorator: + return json.dumps(decorator, indent=2) + else: + return f"decorator '{name}' not found" + except ToreroExecutorError as e: + return f"error getting decorator '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error getting decorator '{name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error getting decorator '{name}'") + return f"unexpected error: {e}" -async def list_decorator_types(client: ToreroClient) -> str: +async def list_decorator_types(executor: ToreroExecutor) -> str: """ - Get all available decorator types. + get all available decorator types. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing list of decorator types + returns: + json string containing list of decorator types """ try: - types = await client.list_decorator_types() + decorators = await executor.get_decorators() + types = sorted(set(d.get('type', 'unknown') for d in decorators)) return json.dumps(types, indent=2) - except ToreroAPIError as e: - return f"Error listing decorator types: {e}" + except ToreroExecutorError as e: + return f"error listing decorator types: {e}" except Exception as e: - logger.exception("Unexpected error in list_decorator_types") - return f"Unexpected error: {e}" \ No newline at end of file + logger.exception("unexpected error in list_decorator_types") + return f"unexpected error: {e}" \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/execution_tools.py b/opt/torero-mcp/torero_mcp/tools/execution_tools.py index 0cc0db4..eea7ef4 100644 --- a/opt/torero-mcp/torero_mcp/tools/execution_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/execution_tools.py @@ -1,226 +1,104 @@ -"""Service execution tools for torero MCP server.""" +"""service execution tools for torero mcp server.""" import json import logging from typing import Any, Dict, Optional -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) -async def execute_ansible_playbook(client: ToreroClient, service_name: str) -> str: +async def execute_ansible_playbook(executor: ToreroExecutor, service_name: str) -> str: """ - Execute an ansible-playbook service. + execute an ansible-playbook service. - Args: - client: ToreroClient instance - service_name: Name of the ansible-playbook service to execute + args: + executor: toreroexecutor instance + service_name: name of the ansible-playbook service to execute - Returns: - JSON string containing execution result with return_code, stdout, stderr, and timing information + returns: + json string containing execution result with return_code, stdout, stderr, and timing information """ try: - result = await client.execute_ansible_playbook(service_name=service_name) + result = await executor.run_ansible_playbook_service(service_name) return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error executing ansible-playbook service '{service_name}': {e}" + except ToreroExecutorError as e: + return f"error executing ansible-playbook service '{service_name}': {e}" except Exception as e: - logger.exception(f"Unexpected error executing ansible-playbook service '{service_name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error executing ansible-playbook service '{service_name}'") + return f"unexpected error: {e}" -async def execute_python_script(client: ToreroClient, service_name: str) -> str: +async def execute_python_script(executor: ToreroExecutor, service_name: str) -> str: """ - Execute a python-script service. + execute a python-script service. - Args: - client: ToreroClient instance - service_name: Name of the python-script service to execute + args: + executor: toreroexecutor instance + service_name: name of the python-script service to execute - Returns: - JSON string containing execution result with return_code, stdout, stderr, and timing information + returns: + json string containing execution result with return_code, stdout, stderr, and timing information """ try: - result = await client.execute_python_script(service_name=service_name) + result = await executor.run_python_script_service(service_name) return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error executing python-script service '{service_name}': {e}" + except ToreroExecutorError as e: + return f"error executing python-script service '{service_name}': {e}" except Exception as e: - logger.exception(f"Unexpected error executing python-script service '{service_name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error executing python-script service '{service_name}'") + return f"unexpected error: {e}" -async def execute_opentofu_plan_apply(client: ToreroClient, service_name: str) -> str: +async def execute_opentofu_plan_apply(executor: ToreroExecutor, service_name: str) -> str: """ - Execute an OpenTofu plan service to apply infrastructure changes. + execute an opentofu plan service to apply infrastructure changes. - Args: - client: ToreroClient instance - service_name: Name of the OpenTofu plan service to apply + args: + executor: toreroexecutor instance + service_name: name of the opentofu plan service to apply - Returns: - JSON string containing execution result with return_code, stdout, stderr, and timing information + returns: + json string containing execution result with return_code, stdout, stderr, and timing information """ try: - result = await client.execute_opentofu_plan_apply(service_name=service_name) + result = await executor.run_opentofu_plan_apply_service(service_name) return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error executing OpenTofu plan apply service '{service_name}': {e}" + except ToreroExecutorError as e: + return f"error executing opentofu plan apply service '{service_name}': {e}" except Exception as e: - logger.exception(f"Unexpected error executing OpenTofu plan apply service '{service_name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error executing opentofu plan apply service '{service_name}'") + return f"unexpected error: {e}" -async def execute_opentofu_plan_destroy(client: ToreroClient, service_name: str) -> str: +async def execute_opentofu_plan_destroy(executor: ToreroExecutor, service_name: str) -> str: """ - Execute an OpenTofu plan service to destroy infrastructure resources. + execute an opentofu plan service to destroy infrastructure resources. - Args: - client: ToreroClient instance - service_name: Name of the OpenTofu plan service to destroy + args: + executor: toreroexecutor instance + service_name: name of the opentofu plan service to destroy - Returns: - JSON string containing execution result with return_code, stdout, stderr, and timing information + returns: + json string containing execution result with return_code, stdout, stderr, and timing information """ try: - result = await client.execute_opentofu_plan_destroy(service_name=service_name) + result = await executor.run_opentofu_plan_destroy_service(service_name) return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error executing OpenTofu plan destroy service '{service_name}': {e}" + except ToreroExecutorError as e: + return f"error executing opentofu plan destroy service '{service_name}': {e}" except Exception as e: - logger.exception(f"Unexpected error executing OpenTofu plan destroy service '{service_name}'") - return f"Unexpected error: {e}" - - -async def execute_service( - client: ToreroClient, - name: str, - parameters: Optional[Dict[str, Any]] = None, - async_execution: bool = False, - timeout: Optional[int] = None -) -> str: - """ - Execute a service with given parameters. - - Args: - client: ToreroClient instance - name: Name of the service to execute - parameters: Parameters to pass to the service - async_execution: Whether to execute asynchronously - timeout: Timeout in seconds - - Returns: - JSON string containing execution result - """ - try: - result = await client.execute_service( - name, - parameters, - async_execution=async_execution, - timeout=timeout - ) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error executing service '{name}': {e}" - except Exception as e: - logger.exception(f"Unexpected error executing service '{name}'") - return f"Unexpected error: {e}" - - -async def get_execution_status(client: ToreroClient, execution_id: str) -> str: - """ - Get the status of a service execution. - - Args: - client: ToreroClient instance - execution_id: ID of the execution to check - - Returns: - JSON string containing execution status - """ - try: - result = await client.get_execution_status(execution_id) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error getting execution status for '{execution_id}': {e}" - except Exception as e: - logger.exception(f"Unexpected error getting execution status for '{execution_id}'") - return f"Unexpected error: {e}" - - -async def list_executions( - client: ToreroClient, - service_name: Optional[str] = None, - status: Optional[str] = None, - limit: int = 100 -) -> str: - """ - List service executions with optional filtering. - - Args: - client: ToreroClient instance - service_name: Filter by service name - status: Filter by execution status - limit: Maximum number of executions to return - - Returns: - JSON string containing list of executions - """ - try: - result = await client.list_executions( - service_name=service_name, - status=status, - limit=limit - ) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error listing executions: {e}" - except Exception as e: - logger.exception("Unexpected error listing executions") - return f"Unexpected error: {e}" - - -async def cancel_execution(client: ToreroClient, execution_id: str) -> str: - """ - Cancel a running service execution. - - Args: - client: ToreroClient instance - execution_id: ID of the execution to cancel - - Returns: - JSON string containing cancellation result - """ - try: - result = await client.cancel_execution(execution_id) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error cancelling execution '{execution_id}': {e}" - except Exception as e: - logger.exception(f"Unexpected error cancelling execution '{execution_id}'") - return f"Unexpected error: {e}" - - -async def stream_execution_logs(client: ToreroClient, execution_id: str, follow: bool = True) -> str: - """ - Stream execution logs. - - Args: - client: ToreroClient instance - execution_id: ID of the execution to stream logs for - follow: Whether to follow the logs - - Returns: - Log entries as they come - """ - try: - logs = [] - async for log_entry in client.stream_execution_logs(execution_id, follow=follow): - logs.append(log_entry) - return json.dumps(logs, indent=2) - except ToreroAPIError as e: - return f"Error streaming logs for execution '{execution_id}': {e}" - except Exception as e: - logger.exception(f"Unexpected error streaming logs for execution '{execution_id}'") - return f"Unexpected error: {e}" \ No newline at end of file + logger.exception(f"unexpected error executing opentofu plan destroy service '{service_name}'") + return f"unexpected error: {e}" + + +# note: the following functions were removed as they require api-level execution tracking +# which is not available through direct cli execution: +# - execute_service (generic service execution with parameters) +# - get_execution_status (requires execution id tracking) +# - list_executions (requires execution database) +# - cancel_execution (requires execution tracking) +# - stream_execution_logs (requires execution tracking) +# +# direct service execution is available through the specific service type functions above \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/health_tools.py b/opt/torero-mcp/torero_mcp/tools/health_tools.py index 6203593..e9d1c09 100644 --- a/opt/torero-mcp/torero_mcp/tools/health_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/health_tools.py @@ -1,49 +1,66 @@ -"""Health check tools for torero MCP server.""" +"""health check tools for torero mcp server.""" import json import logging from typing import Any, Dict -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) -async def health_check(client: ToreroClient) -> str: +async def health_check(executor: ToreroExecutor) -> str: """ - Check the health of the torero API. + check the health of the torero cli. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing health status + returns: + json string containing health status """ try: - health = await client.health_check() - return json.dumps(health, indent=2) - except ToreroAPIError as e: - return f"Error checking health: {e}" + is_available, message = executor.check_torero_available() + version = executor.check_torero_version() + + return json.dumps({ + "status": "healthy" if is_available else "unhealthy", + "torero_available": is_available, + "torero_version": version, + "message": message + }, indent=2) except Exception as e: - logger.exception("Unexpected error in health_check") - return f"Unexpected error: {e}" + logger.exception("unexpected error in health_check") + return json.dumps({ + "status": "unhealthy", + "torero_available": False, + "error": f"unexpected error: {e}" + }, indent=2) -async def get_system_info(client: ToreroClient) -> str: +async def get_torero_version(executor: ToreroExecutor) -> str: """ - Get system information from the torero API. + get torero version information. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing system information + returns: + json string containing version information """ try: - info = await client.get_system_info() - return json.dumps(info, indent=2) - except ToreroAPIError as e: - return f"Error getting system info: {e}" + version = executor.check_torero_version() + is_available, message = executor.check_torero_available() + + return json.dumps({ + "version": version, + "available": is_available, + "message": message + }, indent=2) except Exception as e: - logger.exception("Unexpected error in get_system_info") - return f"Unexpected error: {e}" \ No newline at end of file + logger.exception("unexpected error in get_torero_version") + return json.dumps({ + "version": "unknown", + "available": False, + "error": f"unexpected error: {e}" + }, indent=2) \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/loader.py b/opt/torero-mcp/torero_mcp/tools/loader.py index 0c173e7..b4dde3d 100644 --- a/opt/torero-mcp/torero_mcp/tools/loader.py +++ b/opt/torero-mcp/torero_mcp/tools/loader.py @@ -1,4 +1,4 @@ -"""Dynamic tool loader for torero MCP server.""" +"""dynamic tool loader for torero mcp server.""" import importlib import inspect @@ -7,30 +7,30 @@ from pathlib import Path from typing import Any, Callable, Dict, List -from ..client import ToreroClient +from ..executor import ToreroExecutor logger = logging.getLogger(__name__) class ToolLoader: - """Dynamically loads and registers MCP tools.""" + """dynamically loads and registers mcp tools.""" - def __init__(self, client: ToreroClient): + def __init__(self, executor: ToreroExecutor): """ - Initialize the tool loader. + initialize the tool loader. - Args: - client: ToreroClient instance to pass to tools + args: + executor: toreroexecutor instance to pass to tools """ - self.client = client + self.executor = executor self.tools: Dict[str, Callable] = {} def discover_tools(self) -> List[str]: """ - Discover all tool modules in the tools directory. + discover all tool modules in the tools directory. - Returns: - List of module names containing tools + returns: + list of module names containing tools """ tools_dir = Path(__file__).parent tool_modules = [] @@ -39,18 +39,18 @@ def discover_tools(self) -> List[str]: module_name = file_path.stem tool_modules.append(f"torero_mcp.tools.{module_name}") - logger.info(f"Discovered tool modules: {tool_modules}") + logger.info(f"discovered tool modules: {tool_modules}") return tool_modules def load_tools_from_module(self, module_name: str) -> Dict[str, Callable]: """ - Load all async functions from a module as tools. + load all async functions from a module as tools. - Args: - module_name: Name of the module to load tools from + args: + module_name: name of the module to load tools from - Returns: - Dictionary mapping tool names to functions + returns: + dictionary mapping tool names to functions """ try: module = importlib.import_module(module_name) @@ -59,54 +59,54 @@ def load_tools_from_module(self, module_name: str) -> Dict[str, Callable]: for name, obj in inspect.getmembers(module): if (inspect.iscoroutinefunction(obj) and not name.startswith('_') and - name != 'client'): # Exclude private functions and client + name != 'executor'): # exclude private functions and executor - # Store the original function with its client parameter + # store the original function with its executor parameter tools[name] = obj - logger.debug(f"Loaded tool: {name} from {module_name}") + logger.debug(f"loaded tool: {name} from {module_name}") return tools except Exception as e: - logger.error(f"Failed to load tools from {module_name}: {e}") + logger.error(f"failed to load tools from {module_name}: {e}") return {} def load_all_tools(self) -> Dict[str, Callable]: """ - Load all tools from all discovered modules. + load all tools from all discovered modules. - Returns: - Dictionary mapping tool names to functions + returns: + dictionary mapping tool names to functions """ all_tools = {} for module_name in self.discover_tools(): module_tools = self.load_tools_from_module(module_name) - # Check for name conflicts + # check for name conflicts for tool_name, tool_func in module_tools.items(): if tool_name in all_tools: - logger.warning(f"Tool name conflict: {tool_name} found in multiple modules") + logger.warning(f"tool name conflict: {tool_name} found in multiple modules") all_tools[tool_name] = tool_func self.tools = all_tools - logger.info(f"Loaded {len(all_tools)} tools: {list(all_tools.keys())}") + logger.info(f"loaded {len(all_tools)} tools: {list(all_tools.keys())}") return all_tools def get_tool(self, name: str) -> Callable: """ - Get a specific tool by name. + get a specific tool by name. - Args: - name: Name of the tool to retrieve + args: + name: name of the tool to retrieve - Returns: - Tool function + returns: + tool function - Raises: - KeyError: If tool is not found + raises: + keyerror: if tool is not found """ if name not in self.tools: - raise KeyError(f"Tool '{name}' not found. Available tools: {list(self.tools.keys())}") + raise KeyError(f"tool '{name}' not found. available tools: {list(self.tools.keys())}") return self.tools[name] \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/repository_tools.py b/opt/torero-mcp/torero_mcp/tools/repository_tools.py index ec67c78..e140fad 100644 --- a/opt/torero-mcp/torero_mcp/tools/repository_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/repository_tools.py @@ -1,163 +1,103 @@ -"""Repository-related tools for torero MCP server.""" +"""repository-related tools for torero mcp server.""" import json import logging from typing import Any, Dict, Optional -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) async def list_repositories( - client: ToreroClient, + executor: ToreroExecutor, repo_type: Optional[str] = None, tag: Optional[str] = None, limit: int = 100 ) -> str: """ - List torero repositories with optional filtering. + list torero repositories with optional filtering. - Args: - client: ToreroClient instance - repo_type: Filter by repository type (e.g., 'file', 'git', 's3') - tag: Filter by tag - limit: Maximum number of repositories to return (default: 100) + args: + executor: toreroexecutor instance + repo_type: filter by repository type (e.g., 'file', 'git', 's3') + tag: filter by tag + limit: maximum number of repositories to return (default: 100) - Returns: - JSON string containing list of repositories + returns: + json string containing list of repositories """ try: - repositories = await client.list_repositories( - repo_type=repo_type, - tag=tag, - limit=limit - ) + repositories = await executor.get_repositories() + + # apply filters + if repo_type: + repositories = [r for r in repositories if r.get('type') == repo_type] + if tag: + repositories = [r for r in repositories if tag in r.get('tags', [])] + + # apply limit + repositories = repositories[:limit] + return json.dumps(repositories, indent=2) - except ToreroAPIError as e: - return f"Error listing repositories: {e}" + except ToreroExecutorError as e: + return f"error listing repositories: {e}" except Exception as e: - logger.exception("Unexpected error in list_repositories") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_repositories") + return f"unexpected error: {e}" -async def get_repository(client: ToreroClient, name: str) -> str: +async def get_repository(executor: ToreroExecutor, name: str) -> str: """ - Get detailed information about a specific torero repository. + get detailed information about a specific torero repository. - Args: - client: ToreroClient instance - name: Name of the repository to retrieve + args: + executor: toreroexecutor instance + name: name of the repository to retrieve - Returns: - JSON string containing repository details + returns: + json string containing repository details """ try: - repository = await client.get_repository(name) - return json.dumps(repository, indent=2) - except ToreroAPIError as e: - return f"Error getting repository '{name}': {e}" + repositories = await executor.get_repositories() + repository = next((r for r in repositories if r.get('name') == name), None) + + if repository: + return json.dumps(repository, indent=2) + else: + return f"repository '{name}' not found" + except ToreroExecutorError as e: + return f"error getting repository '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error getting repository '{name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error getting repository '{name}'") + return f"unexpected error: {e}" -async def list_repository_types(client: ToreroClient) -> str: +async def list_repository_types(executor: ToreroExecutor) -> str: """ - Get all available repository types. + get all available repository types. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing list of repository types + returns: + json string containing list of repository types """ try: - types = await client.list_repository_types() + repositories = await executor.get_repositories() + types = sorted(set(r.get('type', 'unknown') for r in repositories)) return json.dumps(types, indent=2) - except ToreroAPIError as e: - return f"Error listing repository types: {e}" - except Exception as e: - logger.exception("Unexpected error in list_repository_types") - return f"Unexpected error: {e}" - - -async def sync_repository(client: ToreroClient, name: str, force: bool = False) -> str: - """ - Sync a repository with its remote source. - - Args: - client: ToreroClient instance - name: Name of the repository to sync - force: Force sync even if there are conflicts - - Returns: - JSON string containing sync result - """ - try: - result = await client.sync_repository(name, force=force) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error syncing repository '{name}': {e}" + except ToreroExecutorError as e: + return f"error listing repository types: {e}" except Exception as e: - logger.exception(f"Unexpected error syncing repository '{name}'") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_repository_types") + return f"unexpected error: {e}" -async def create_repository( - client: ToreroClient, - name: str, - url: str, - repo_type: str = "git", - description: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None -) -> str: - """ - Create a new repository. - - Args: - client: ToreroClient instance - name: Name of the repository - url: URL of the repository - repo_type: Type of repository (default: "git") - description: Optional description of the repository - metadata: Optional metadata dictionary - - Returns: - JSON string containing creation result - """ - try: - kwargs = {"type": repo_type} - if description is not None: - kwargs["description"] = description - if metadata is not None: - kwargs["metadata"] = metadata - - result = await client.create_repository(name, url, **kwargs) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error creating repository '{name}': {e}" - except Exception as e: - logger.exception(f"Unexpected error creating repository '{name}'") - return f"Unexpected error: {e}" - - -async def delete_repository(client: ToreroClient, name: str) -> str: - """ - Delete a repository. - - Args: - client: ToreroClient instance - name: Name of the repository to delete - - Returns: - JSON string containing deletion result - """ - try: - result = await client.delete_repository(name) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error deleting repository '{name}': {e}" - except Exception as e: - logger.exception(f"Unexpected error deleting repository '{name}'") - return f"Unexpected error: {e}" \ No newline at end of file +# note: the following functions require api-level repository management +# which is not available through direct cli operations: +# - sync_repository (requires api-level sync operations) +# - create_repository (requires api-level creation) +# - delete_repository (requires api-level deletion) +# +# repository information access is available through the functions above \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/secret_tools.py b/opt/torero-mcp/torero_mcp/tools/secret_tools.py index ff91754..b5e0d6a 100644 --- a/opt/torero-mcp/torero_mcp/tools/secret_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/secret_tools.py @@ -1,178 +1,107 @@ -"""Secret-related tools for torero MCP server.""" +"""secret-related tools for torero mcp server.""" import json import logging from typing import Any, Dict, Optional -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) async def list_secrets( - client: ToreroClient, + executor: ToreroExecutor, secret_type: Optional[str] = None, tag: Optional[str] = None, limit: int = 100 ) -> str: """ - List torero secrets with optional filtering (metadata only). + list torero secrets with optional filtering (metadata only). - Args: - client: ToreroClient instance - secret_type: Filter by secret type (e.g., 'password', 'api-key', 'token') - tag: Filter by tag - limit: Maximum number of secrets to return (default: 100) + args: + executor: toreroexecutor instance + secret_type: filter by secret type (e.g., 'password', 'api-key', 'token') + tag: filter by tag + limit: maximum number of secrets to return (default: 100) - Returns: - JSON string containing list of secret metadata + returns: + json string containing list of secret metadata """ try: - secrets = await client.list_secrets( - secret_type=secret_type, - tag=tag, - limit=limit - ) + secrets = await executor.get_secrets() + + # apply filters + if secret_type: + secrets = [s for s in secrets if s.get('type') == secret_type] + if tag: + secrets = [s for s in secrets if tag in s.get('tags', [])] + + # apply limit + secrets = secrets[:limit] + return json.dumps(secrets, indent=2) - except ToreroAPIError as e: - return f"Error listing secrets: {e}" + except ToreroExecutorError as e: + return f"error listing secrets: {e}" except Exception as e: - logger.exception("Unexpected error in list_secrets") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_secrets") + return f"unexpected error: {e}" -async def get_secret(client: ToreroClient, name: str, include_value: bool = False) -> str: +async def get_secret(executor: ToreroExecutor, name: str, include_value: bool = False) -> str: """ - Get detailed information about a specific torero secret. + get detailed information about a specific torero secret. - Args: - client: ToreroClient instance - name: Name of the secret to retrieve - include_value: Whether to include the secret value in the response + args: + executor: toreroexecutor instance + name: name of the secret to retrieve + include_value: whether to include the secret value (note: not supported via cli) - Returns: - JSON string containing secret metadata (and optionally value) + returns: + json string containing secret metadata """ try: - secret = await client.get_secret(name, include_value=include_value) - return json.dumps(secret, indent=2) - except ToreroAPIError as e: - return f"Error getting secret '{name}': {e}" + secrets = await executor.get_secrets() + secret = next((s for s in secrets if s.get('name') == name), None) + + if secret: + if include_value: + # note: cli doesn't expose secret values for security + secret['note'] = 'secret values not exposed via cli for security' + return json.dumps(secret, indent=2) + else: + return f"secret '{name}' not found" + except ToreroExecutorError as e: + return f"error getting secret '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error getting secret '{name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error getting secret '{name}'") + return f"unexpected error: {e}" -async def list_secret_types(client: ToreroClient) -> str: +async def list_secret_types(executor: ToreroExecutor) -> str: """ - Get all available secret types. + get all available secret types. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing list of secret types + returns: + json string containing list of secret types """ try: - types = await client.list_secret_types() + secrets = await executor.get_secrets() + types = sorted(set(s.get('type', 'unknown') for s in secrets)) return json.dumps(types, indent=2) - except ToreroAPIError as e: - return f"Error listing secret types: {e}" - except Exception as e: - logger.exception("Unexpected error in list_secret_types") - return f"Unexpected error: {e}" - - -async def create_secret( - client: ToreroClient, - name: str, - value: str, - secret_type: str = "generic", - description: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None -) -> str: - """ - Create a new secret. - - Args: - client: ToreroClient instance - name: Name of the secret - value: Secret value - secret_type: Type of secret (default: "generic") - description: Optional description of the secret - metadata: Optional metadata dictionary - - Returns: - JSON string containing creation result - """ - try: - kwargs = {} - if description is not None: - kwargs["description"] = description - if metadata is not None: - kwargs["metadata"] = metadata - - result = await client.create_secret(name, value, secret_type, **kwargs) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error creating secret '{name}': {e}" - except Exception as e: - logger.exception(f"Unexpected error creating secret '{name}'") - return f"Unexpected error: {e}" - - -async def update_secret( - client: ToreroClient, - name: str, - value: Optional[str] = None, - description: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None -) -> str: - """ - Update a secret. - - Args: - client: ToreroClient instance - name: Name of the secret to update - value: New secret value (optional) - description: Updated description (optional) - metadata: Updated metadata dictionary (optional) - - Returns: - JSON string containing update result - """ - try: - kwargs = {} - if description is not None: - kwargs["description"] = description - if metadata is not None: - kwargs["metadata"] = metadata - - result = await client.update_secret(name, value, **kwargs) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error updating secret '{name}': {e}" + except ToreroExecutorError as e: + return f"error listing secret types: {e}" except Exception as e: - logger.exception(f"Unexpected error updating secret '{name}'") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_secret_types") + return f"unexpected error: {e}" -async def delete_secret(client: ToreroClient, name: str) -> str: - """ - Delete a secret. - - Args: - client: ToreroClient instance - name: Name of the secret to delete - - Returns: - JSON string containing deletion result - """ - try: - result = await client.delete_secret(name) - return json.dumps(result, indent=2) - except ToreroAPIError as e: - return f"Error deleting secret '{name}': {e}" - except Exception as e: - logger.exception(f"Unexpected error deleting secret '{name}'") - return f"Unexpected error: {e}" \ No newline at end of file +# note: the following functions require api-level secret management +# which is not available through direct cli operations: +# - create_secret (requires api-level creation) +# - update_secret (requires api-level updates) +# - delete_secret (requires api-level deletion) +# +# secret information access is available through the functions above \ No newline at end of file diff --git a/opt/torero-mcp/torero_mcp/tools/service_tools.py b/opt/torero-mcp/torero_mcp/tools/service_tools.py index 2d539e2..3b28031 100644 --- a/opt/torero-mcp/torero_mcp/tools/service_tools.py +++ b/opt/torero-mcp/torero_mcp/tools/service_tools.py @@ -1,144 +1,161 @@ -"""Service-related tools for torero MCP server.""" +"""service-related tools for torero mcp server.""" import json import logging from typing import Any, Dict, Optional -from ..client import ToreroAPIError, ToreroClient +from ..executor import ToreroExecutorError, ToreroExecutor logger = logging.getLogger(__name__) async def list_services( - client: ToreroClient, + executor: ToreroExecutor, service_type: Optional[str] = None, tag: Optional[str] = None, limit: int = 100 ) -> str: """ - List torero services with optional filtering. + list torero services with optional filtering. - Args: - client: ToreroClient instance - service_type: Filter by service type (e.g., 'ansible-playbook', 'opentofu-plan', 'python-script') - tag: Filter by tag (e.g., 'network', 'backup', 'automation') - limit: Maximum number of services to return (default: 100) + args: + executor: toreroexecutor instance + service_type: filter by service type (e.g., 'ansible-playbook', 'opentofu-plan', 'python-script') + tag: filter by tag (e.g., 'network', 'backup', 'automation') + limit: maximum number of services to return (default: 100) - Returns: - JSON string containing list of services + returns: + json string containing list of services """ try: - services = await client.list_services( - service_type=service_type, - tag=tag, - limit=limit - ) + services = await executor.get_services() + + # apply filters + if service_type: + services = [s for s in services if s.get('type') == service_type] + if tag: + services = [s for s in services if tag in s.get('tags', [])] + + # apply limit + services = services[:limit] + return json.dumps(services, indent=2) - except ToreroAPIError as e: - return f"Error listing services: {e}" + except ToreroExecutorError as e: + return f"error listing services: {e}" except Exception as e: - logger.exception("Unexpected error in list_services") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_services") + return f"unexpected error: {e}" -async def get_service(client: ToreroClient, name: str) -> str: +async def get_service(executor: ToreroExecutor, name: str) -> str: """ - Get detailed information about a specific torero service. + get detailed information about a specific torero service. - Args: - client: ToreroClient instance - name: Name of the service to retrieve + args: + executor: toreroexecutor instance + name: name of the service to retrieve - Returns: - JSON string containing service details + returns: + json string containing service details """ try: - service = await client.get_service(name) - return json.dumps(service, indent=2) - except ToreroAPIError as e: - return f"Error getting service '{name}': {e}" + service = await executor.get_service_by_name(name) + if service: + return json.dumps(service, indent=2) + else: + return f"service '{name}' not found" + except ToreroExecutorError as e: + return f"error getting service '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error getting service '{name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error getting service '{name}'") + return f"unexpected error: {e}" -async def describe_service(client: ToreroClient, name: str) -> str: +async def describe_service(executor: ToreroExecutor, name: str) -> str: """ - Get complete and detailed description of a specific torero service. + get complete and detailed description of a specific torero service. - Args: - client: ToreroClient instance - name: Name of the service to describe + args: + executor: toreroexecutor instance + name: name of the service to describe - Returns: - JSON string containing detailed service description + returns: + json string containing detailed service description """ try: - description = await client.describe_service(name) - return json.dumps(description, indent=2) - except ToreroAPIError as e: - return f"Error describing service '{name}': {e}" + description = await executor.describe_service(name) + if description: + return json.dumps(description, indent=2) + else: + return f"service '{name}' description not available" + except ToreroExecutorError as e: + return f"error describing service '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error describing service '{name}'") - return f"Unexpected error: {e}" + logger.exception(f"unexpected error describing service '{name}'") + return f"unexpected error: {e}" -async def list_service_types(client: ToreroClient) -> str: +async def list_service_types(executor: ToreroExecutor) -> str: """ - Get all available service types. + get all available service types. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing list of service types + returns: + json string containing list of service types """ try: - types = await client.list_service_types() + services = await executor.get_services() + types = sorted(set(s.get('type', 'unknown') for s in services)) return json.dumps(types, indent=2) - except ToreroAPIError as e: - return f"Error listing service types: {e}" + except ToreroExecutorError as e: + return f"error listing service types: {e}" except Exception as e: - logger.exception("Unexpected error in list_service_types") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_service_types") + return f"unexpected error: {e}" -async def list_service_tags(client: ToreroClient) -> str: +async def list_service_tags(executor: ToreroExecutor) -> str: """ - Get all available service tags. + get all available service tags. - Args: - client: ToreroClient instance + args: + executor: toreroexecutor instance - Returns: - JSON string containing list of service tags + returns: + json string containing list of service tags """ try: - tags = await client.list_service_tags() + services = await executor.get_services() + tags = sorted(set(tag for s in services for tag in s.get('tags', []))) return json.dumps(tags, indent=2) - except ToreroAPIError as e: - return f"Error listing service tags: {e}" + except ToreroExecutorError as e: + return f"error listing service tags: {e}" except Exception as e: - logger.exception("Unexpected error in list_service_tags") - return f"Unexpected error: {e}" + logger.exception("unexpected error in list_service_tags") + return f"unexpected error: {e}" -async def get_service_description(client: ToreroClient, name: str) -> str: +async def get_service_description(executor: ToreroExecutor, name: str) -> str: """ - Get detailed description of a specific torero service. + get detailed description of a specific torero service. - Args: - client: ToreroClient instance - name: Name of the service to get description for + args: + executor: toreroexecutor instance + name: name of the service to get description for - Returns: - JSON string containing service description + returns: + json string containing service description """ try: - description = await client.get_service_description(name) - return json.dumps(description, indent=2) - except ToreroAPIError as e: - return f"Error getting service description for '{name}': {e}" + description = await executor.describe_service(name) + if description: + return json.dumps(description, indent=2) + else: + return f"service '{name}' description not available" + except ToreroExecutorError as e: + return f"error getting service description for '{name}': {e}" except Exception as e: - logger.exception(f"Unexpected error getting service description for '{name}'") - return f"Unexpected error: {e}" \ No newline at end of file + logger.exception(f"unexpected error getting service description for '{name}'") + return f"unexpected error: {e}" \ No newline at end of file diff --git a/opt/torero-ui/torero_ui/dashboard/services.py b/opt/torero-ui/torero_ui/dashboard/services.py index a55552e..c5d0ec2 100644 --- a/opt/torero-ui/torero_ui/dashboard/services.py +++ b/opt/torero-ui/torero_ui/dashboard/services.py @@ -1,11 +1,11 @@ -"""services for interacting with torero api.""" +"""services for interacting with torero cli.""" import json import logging +import subprocess from datetime import datetime from typing import Any, Dict, List, Optional -import requests from dateutil import parser as date_parser from django.conf import settings from django.utils import timezone @@ -15,64 +15,82 @@ logger = logging.getLogger(__name__) -class ToreroAPIClient: - """client for interacting with torero api.""" +class ToreroCliClient: + """client for interacting with torero cli directly.""" def __init__(self) -> None: - self.base_url = settings.TORERO_API_BASE_URL.rstrip('/') - self.timeout = settings.TORERO_API_TIMEOUT - self.session = requests.Session() + self.torero_command = "torero" + self.timeout = getattr(settings, 'TORERO_CLI_TIMEOUT', 30) - def _make_request(self, endpoint: str, method: str = "GET", **kwargs: Any) -> Optional[Dict[str, Any]]: - """make http request to torero api.""" - url = f"{self.base_url}{endpoint}" + def _execute_command(self, args: List[str]) -> Optional[Dict[str, Any]]: + """execute torero cli command and return parsed json output.""" + command = [self.torero_command] + args + ["--raw"] try: - response = self.session.request( - method=method, - url=url, + result = subprocess.run( + command, + capture_output=True, + text=True, timeout=self.timeout, - **kwargs + check=False ) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logger.error(f"torero api request failed: {url} - {e}") + + if result.returncode == 0 and result.stdout: + try: + return json.loads(result.stdout) + except json.JSONDecodeError: + logger.warning(f"failed to parse json from torero output: {result.stdout[:200]}") + return None + else: + logger.error(f"torero command failed: {' '.join(command)}\nError: {result.stderr}") + return None + + except subprocess.TimeoutExpired: + logger.error(f"torero command timed out: {' '.join(command)}") + return None + except Exception as e: + logger.error(f"failed to execute torero command: {e}") return None def get_services(self) -> List[Dict[str, Any]]: - """get list of all services.""" - data = self._make_request("/v1/services/") - return data if isinstance(data, list) else [] + """get list of all services from torero cli.""" + data = self._execute_command(["get", "services"]) + if data and isinstance(data, dict) and "services" in data: + return data["services"] if isinstance(data["services"], list) else [] + return [] def get_service_details(self, service_name: str) -> Optional[Dict[str, Any]]: - """get details for specific service.""" - return self._make_request(f"/v1/services/{service_name}") + """get details for specific service from torero cli.""" + return self._execute_command(["describe", "service", service_name]) def execute_service(self, service_name: str, service_type: str, **params: Any) -> Optional[Dict[str, Any]]: - """execute a service and return execution data.""" - endpoint_map = { - "ansible-playbook": f"/v1/execution/ansible-playbook/{service_name}", - "python-script": f"/v1/execution/python-script/{service_name}", - "opentofu-plan": f"/v1/execution/opentofu-plan/{service_name}/apply", - } - - endpoint = endpoint_map.get(service_type) - if not endpoint: + """execute a service via torero cli.""" + # build command based on service type + if service_type == "ansible-playbook": + command = ["run", "service", "ansible-playbook", service_name] + elif service_type == "python-script": + command = ["run", "service", "python-script", service_name] + elif service_type == "opentofu-plan": + command = ["run", "service", "opentofu-plan", service_name] + else: logger.error(f"unsupported service type: {service_type}") return None - return self._make_request(endpoint, method="POST", json=params) + # add any additional parameters + for key, value in params.items(): + command.extend([f"--{key}", str(value)]) + + return self._execute_command(command) class DataCollectionService: """service for collecting and storing torero execution data.""" def __init__(self) -> None: - self.api_client = ToreroAPIClient() + self.cli_client = ToreroCliClient() def sync_services(self) -> None: - """synchronize service information from torero api.""" - services = self.api_client.get_services() + """synchronize service information from torero cli.""" + services = self.cli_client.get_services() for service_data in services: service_name = service_data.get("name") @@ -80,7 +98,7 @@ def sync_services(self) -> None: continue # get detailed service info - service_details = self.api_client.get_service_details(service_name) + service_details = self.cli_client.get_service_details(service_name) if not service_details: continue diff --git a/opt/torero-ui/torero_ui/settings.py b/opt/torero-ui/torero_ui/settings.py index 9c1fbb5..fe60f21 100644 --- a/opt/torero-ui/torero_ui/settings.py +++ b/opt/torero-ui/torero_ui/settings.py @@ -86,9 +86,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -# torero api configuration -TORERO_API_BASE_URL = os.environ.get("TORERO_API_BASE_URL", "http://localhost:8000") -TORERO_API_TIMEOUT = int(os.environ.get("TORERO_API_TIMEOUT", "30")) +# torero cli configuration +TORERO_CLI_TIMEOUT = int(os.environ.get("TORERO_CLI_TIMEOUT", "30")) # dashboard refresh interval in seconds DASHBOARD_REFRESH_INTERVAL = int(os.environ.get("UI_REFRESH_INTERVAL", "30")) \ No newline at end of file From edba1f904651b17c31f3448085ddad0e5d996c9b Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:13:15 -0400 Subject: [PATCH 07/13] Update tests.md --- docs/tests.md | 151 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 53 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index a857080..9c31992 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -4,7 +4,8 @@ This guide provides step-by-step instructions for running tests across the torer ## Table of Contents - [Prerequisites](#prerequisites) - [Running All Tests](#running-all-tests) -- [Testing torero API](#testing-torero-api) +- [Testing torero MCP](#testing-torero-mcp) +- [Testing torero UI](#testing-torero-ui) - [Test Coverage](#test-coverage) - [Common Issues](#common-issues) @@ -18,7 +19,7 @@ Before running tests, ensure you have the following installed: ### Quick Start -The easiest way to run tests for torero API is using the provided script from the root directory: +The easiest way to run tests is using the provided script from the root directory: ```bash # from the torero-container root directory @@ -30,7 +31,7 @@ This script will: 2. Install dependencies if needed 3. Run all tests with coverage reporting -## Testing torero API +## Testing torero MCP ### Step-by-Step Setup @@ -41,7 +42,7 @@ This script will: 2. **Install development dependencies:** ```bash - cd opt/torero-api + cd opt/torero-mcp uv pip install -e ".[dev]" ``` @@ -49,13 +50,11 @@ This script will: - pytest (test framework) - pytest-asyncio (async test support) - pytest-cov (coverage reporting) - - httpx (HTTP client for testing) - Other development tools (black, flake8, isort) -3. **Run tests from the root directory:** +3. **Run tests:** ```bash - cd ../.. # back to project root - ./tools.sh --test + uv run pytest tests/ -v ``` ### Alternative Methods @@ -63,31 +62,57 @@ This script will: #### Method 1: Direct pytest execution ```bash # from torero-container root -pytest tests/ -v +uv run pytest opt/torero-mcp/tests/ -v ``` #### Method 2: With specific options ```bash # run with coverage report -pytest tests/ --cov=opt/torero-api/torero_api --cov-report=term-missing +uv run pytest opt/torero-mcp/tests/ --cov=opt/torero-mcp/torero_mcp --cov-report=term-missing # stop on first failure -pytest tests/ -x +uv run pytest opt/torero-mcp/tests/ -x # run specific test file -pytest tests/test_server.py +uv run pytest opt/torero-mcp/tests/test_executor.py # run tests matching a pattern -pytest tests/ -k "test_health" +uv run pytest opt/torero-mcp/tests/ -k "test_health" # run with maximum verbosity -pytest tests/ -vv +uv run pytest opt/torero-mcp/tests/ -vv ``` -#### Method 3: From the torero-api directory +## Testing torero UI + +### Step-by-Step Setup + +1. **Navigate to the UI directory:** + ```bash + cd opt/torero-ui + ``` + +2. **Install development dependencies:** + ```bash + uv pip install -e ".[dev]" + ``` + +3. **Run Django tests:** + ```bash + uv run python torero_ui/manage.py test + ``` + +### Alternative Methods + +#### Method 1: Using pytest with Django ```bash -cd opt/torero-api -pytest ../../tests/ -v +# from torero-ui directory +uv run pytest --ds=torero_ui.settings +``` + +#### Method 2: Test specific apps +```bash +uv run python torero_ui/manage.py test dashboard ``` ## Test Coverage @@ -105,38 +130,32 @@ The test suite generates three types of coverage reports: ### Current Test Coverage -As of the latest run: -- **Total Coverage**: ~53% -- **Test Count**: 77 tests -- **Components Tested**: - - Core functionality (torero executor) - - Database operations (import/export) - - API endpoints (services, decorators, repositories, secrets) - - Server health checks - - Service descriptions +Test coverage varies by component: +- **torero-mcp**: Direct CLI executor and MCP tools +- **torero-ui**: Django views, models, and service sync ### Coverage by Module -| Module | Coverage | Key Areas | -|--------|----------|-----------| -| API Endpoints | 65-82% | Services, database, decorators | -| Models | 89-100% | Data models and schemas | -| Server | 82% | Application setup and routing | -| Core Executor | 32% | Command execution logic | +| Module | Component | Key Areas | +|--------|-----------|-----------| +| MCP Executor | torero-mcp | CLI command execution, parsing | +| MCP Tools | torero-mcp | Service, database, health tools | +| UI Models | torero-ui | ServiceInfo, ServiceExecution | +| UI Services | torero-ui | ToreroCliClient, sync services | +| UI Views | torero-ui | Dashboard, API endpoints | ## Common Issues ### Issue 1: pytest command not found -**Solution**: Install development dependencies +**Solution**: Install pytest using uv ```bash -cd opt/torero-api -uv pip install -e ".[dev]" +uv pip install pytest pytest-asyncio pytest-cov ``` ### Issue 2: Virtual environment not activated -**Solution**: The `tools.sh` script handles this automatically. For manual testing: +**Solution**: With uv, virtual environments are handled automatically. For manual activation: ```bash source .venv/bin/activate ``` @@ -157,13 +176,21 @@ cd /path/to/torero-container chmod +x tools.sh ``` +### Issue 5: torero CLI not found + +**Solution**: Tests that use ToreroExecutor or ToreroCliClient require torero to be installed +```bash +# Install torero or run tests in container +docker compose -f docker-compose.dev.yml exec torero uv run pytest +``` + ## Advanced Testing ### Running with different Python versions ```bash -# using uv to test with specific Python version -uv run --python 3.11 pytest tests/ +# using specific Python version with uv +uv run --python 3.11 pytest opt/torero-mcp/tests/ ``` ### Parallel test execution @@ -173,17 +200,17 @@ uv run --python 3.11 pytest tests/ uv pip install pytest-xdist # run tests in parallel -pytest tests/ -n auto +uv run pytest opt/torero-mcp/tests/ -n auto ``` ### Debugging failed tests ```bash # drop into debugger on failures -pytest tests/ --pdb +uv run pytest opt/torero-mcp/tests/ --pdb # show local variables for failed tests -pytest tests/ -l +uv run pytest opt/torero-mcp/tests/ -l ``` ## Continuous Integration @@ -191,33 +218,51 @@ pytest tests/ -l For CI/CD pipelines, use the XML coverage report: ```bash -pytest tests/ --cov=opt/torero-api/torero_api --cov-report=xml +uv run pytest opt/torero-mcp/tests/ --cov=opt/torero-mcp/torero_mcp --cov-report=xml ``` The `coverage.xml` file can be uploaded to services like Codecov or Coveralls. ## Writing New Tests -Tests are located in the `tests/` directory at the root level. Follow these conventions: +Tests are organized by component: +- **MCP Tests**: `opt/torero-mcp/tests/` +- **UI Tests**: `opt/torero-ui/tests/` or `opt/torero-ui/torero_ui/dashboard/tests.py` + +Follow these conventions: 1. **File naming**: `test_.py` 2. **Test function naming**: `test_` 3. **Test classes**: `Test` -4. **Use fixtures**: Define reusable test fixtures in `tests/conftest.py` +4. **Use fixtures**: Define reusable test fixtures in `conftest.py` -Example test structure: +Example test structure for MCP: ```python import pytest -from fastapi.testclient import TestClient +from torero_mcp.executor import ToreroExecutor -def test_endpoint_success(client): - response = client.get("/api/v1/endpoint") - assert response.status_code == 200 - assert response.json()["status"] == "success" +@pytest.mark.asyncio +async def test_executor_get_services(): + executor = ToreroExecutor(timeout=30) + services = await executor.get_services() + assert isinstance(services, list) +``` + +Example test structure for UI: +```python +from django.test import TestCase +from torero_ui.dashboard.services import ToreroCliClient + +class TestCliClient(TestCase): + def test_get_services(self): + client = ToreroCliClient() + services = client.get_services() + self.assertIsInstance(services, list) ``` -## Some `TODO` items -- Review test failures and improve coverage -- Add integration tests for complex workflows +## TODO items +- Add integration tests for MCP-CLI interaction +- Add tests for UI sync service +- Improve test coverage for subprocess handling - Set up continuous integration with GitHub Actions - Configure code quality tools (black, flake8, mypy) \ No newline at end of file From 6f03022fc082a51e40cf878a1e865cc78c046706 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:15:49 -0400 Subject: [PATCH 08/13] Remove legacy api references --- opt/torero-mcp/torero_mcp/executor.py | 337 ++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 opt/torero-mcp/torero_mcp/executor.py diff --git a/opt/torero-mcp/torero_mcp/executor.py b/opt/torero-mcp/torero_mcp/executor.py new file mode 100644 index 0000000..3539313 --- /dev/null +++ b/opt/torero-mcp/torero_mcp/executor.py @@ -0,0 +1,337 @@ +"""direct cli executor for torero commands.""" + +import json +import logging +import subprocess +import shutil +from typing import Any, Dict, List, Optional, Tuple +from datetime import datetime + +logger = logging.getLogger(__name__) + +# torero command +TORERO_COMMAND = 'torero' + +class ToreroExecutorError(Exception): + """custom exception for torero executor errors.""" + pass + +class ToreroExecutor: + """direct cli executor for torero commands.""" + + def __init__(self, timeout: int = 30): + """ + initialize the torero executor. + + args: + timeout: default timeout for commands in seconds + """ + self.timeout = timeout + self.torero_command = TORERO_COMMAND + + def check_torero_available(self) -> Tuple[bool, str]: + """ + check if torero is available in the system path. + + returns: + tuple[bool, str]: availability status and message + """ + torero_path = shutil.which(self.torero_command) + if not torero_path: + return False, f"{self.torero_command} executable not found in path" + + try: + result = subprocess.run( + [self.torero_command, "version"], + capture_output=True, + text=True, + check=False, + timeout=5 + ) + + if result.returncode != 0: + return False, f"{self.torero_command} command failed: {result.stderr.strip()}" + + return True, f"{self.torero_command} is available" + except subprocess.TimeoutExpired: + return False, f"{self.torero_command} command timed out" + except Exception as e: + return False, f"error checking {self.torero_command}: {str(e)}" + + def check_torero_version(self) -> str: + """ + get the version of torero installed. + + returns: + str: version of torero, or "unknown" if couldn't be determined + """ + try: + result = subprocess.run( + [self.torero_command, "version"], + capture_output=True, + text=True, + check=False, + timeout=5 + ) + + if result.returncode != 0: + return "unknown" + + # parse version from output + output_lines = result.stdout.strip().split("\n") + for line in output_lines: + if line.startswith("torero"): + parts = line.split() + if len(parts) >= 3: + return parts[2] + + return "unknown" + except Exception: + return "unknown" + + async def execute_command( + self, + args: List[str], + timeout: Optional[int] = None, + parse_json: bool = True + ) -> Any: + """ + execute torero command with given arguments. + + args: + args: command arguments (without 'torero' prefix) + timeout: command timeout (uses instance default if none) + parse_json: whether to parse output as json + + returns: + command output (parsed as json if parse_json=true) + + raises: + toreroexecutorerror: if command fails + """ + command = [self.torero_command] + args + cmd_timeout = timeout or self.timeout + + logger.debug(f"executing command: {' '.join(command)}") + + try: + proc = subprocess.run( + command, + capture_output=True, + text=True, + check=False, + timeout=cmd_timeout + ) + + if proc.returncode != 0: + error_msg = f"torero error: {proc.stderr.strip()}" + logger.error(error_msg) + raise ToreroExecutorError(error_msg) + + if parse_json: + try: + return json.loads(proc.stdout) + except json.JSONDecodeError as e: + error_msg = f"invalid json from torero: {e}" + logger.error(error_msg) + logger.debug(f"raw output: {proc.stdout[:1000]}...") + raise ToreroExecutorError(error_msg) + else: + return proc.stdout + + except subprocess.TimeoutExpired: + error_msg = f"torero command timed out after {cmd_timeout}s" + logger.error(error_msg) + raise ToreroExecutorError(error_msg) + except Exception as e: + if isinstance(e, ToreroExecutorError): + raise + logger.exception(f"unexpected error executing torero command: {str(e)}") + raise ToreroExecutorError(f"failed to execute torero command: {str(e)}") + + # service operations + async def get_services(self) -> List[Dict[str, Any]]: + """get all services.""" + raw_output = await self.execute_command(["get", "services", "--raw"]) + + # handle different response formats + if isinstance(raw_output, dict) and "items" in raw_output: + return raw_output["items"] + elif isinstance(raw_output, list): + return raw_output + else: + raise ToreroExecutorError(f"unexpected json structure: {type(raw_output)}") + + async def get_service_by_name(self, name: str) -> Optional[Dict[str, Any]]: + """get specific service by name.""" + services = await self.get_services() + for service in services: + if service.get("name") == name: + return service + return None + + async def describe_service(self, name: str) -> Optional[Dict[str, Any]]: + """get detailed description of a service.""" + raw_output = await self.execute_command(["describe", "service", name, "--raw"]) + + if isinstance(raw_output, list): + return raw_output + elif isinstance(raw_output, dict): + return [raw_output] # wrap for consistency + else: + return None + + # decorator operations + async def get_decorators(self) -> List[Dict[str, Any]]: + """get all decorators.""" + raw_output = await self.execute_command(["get", "decorators", "--raw"]) + + if isinstance(raw_output, dict) and "decorators" in raw_output: + return raw_output["decorators"] + elif isinstance(raw_output, dict) and "items" in raw_output: + return raw_output["items"] + elif isinstance(raw_output, list): + return raw_output + else: + raise ToreroExecutorError(f"unexpected json structure: {type(raw_output)}") + + async def describe_decorator(self, name: str) -> Optional[Dict[str, Any]]: + """get detailed description of a decorator.""" + return await self.execute_command(["describe", "decorator", name, "--raw"]) + + # repository operations + async def get_repositories(self) -> List[Dict[str, Any]]: + """get all repositories.""" + raw_output = await self.execute_command(["get", "repositories", "--raw"]) + + if isinstance(raw_output, dict) and "items" in raw_output: + return raw_output["items"] + elif isinstance(raw_output, list): + return raw_output + else: + raise ToreroExecutorError(f"unexpected json structure: {type(raw_output)}") + + async def describe_repository(self, name: str) -> Optional[Dict[str, Any]]: + """get detailed description of a repository.""" + return await self.execute_command(["describe", "repository", name, "--raw"]) + + # secret operations + async def get_secrets(self) -> List[Dict[str, Any]]: + """get all secrets.""" + raw_output = await self.execute_command(["get", "secrets", "--raw"]) + + if isinstance(raw_output, dict) and "items" in raw_output: + return raw_output["items"] + elif isinstance(raw_output, list): + return raw_output + else: + raise ToreroExecutorError(f"unexpected json structure: {type(raw_output)}") + + async def describe_secret(self, name: str) -> Optional[Dict[str, Any]]: + """get detailed description of a secret.""" + return await self.execute_command(["describe", "secret", name, "--raw"]) + + # registry operations + async def get_registries(self) -> List[Dict[str, Any]]: + """get all registries.""" + raw_output = await self.execute_command(["get", "registries", "--raw"]) + + if isinstance(raw_output, dict) and "items" in raw_output: + return raw_output["items"] + elif isinstance(raw_output, list): + return raw_output + else: + raise ToreroExecutorError(f"unexpected json structure: {type(raw_output)}") + + # service execution operations + async def run_ansible_playbook_service(self, name: str, **kwargs) -> Dict[str, Any]: + """execute ansible playbook service.""" + command = ["run", "service", "ansible-playbook", name, "--raw"] + + # add additional parameters + for key, value in kwargs.items(): + if value is not None: + command.append(f"--{key}={value}") + + return await self.execute_command(command, timeout=300) # 5 min timeout + + async def run_python_script_service(self, name: str, **kwargs) -> Dict[str, Any]: + """execute python script service.""" + command = ["run", "service", "python-script", name, "--raw"] + + for key, value in kwargs.items(): + if value is not None: + command.append(f"--{key}={value}") + + return await self.execute_command(command, timeout=300) + + async def run_opentofu_plan_apply_service(self, name: str, **kwargs) -> Dict[str, Any]: + """execute opentofu plan apply service.""" + command = ["run", "service", "opentofu-plan", "apply", name, "--raw"] + + for key, value in kwargs.items(): + if value is not None: + command.append(f"--{key}={value}") + + return await self.execute_command(command, timeout=600) # 10 min timeout + + async def run_opentofu_plan_destroy_service(self, name: str, **kwargs) -> Dict[str, Any]: + """execute opentofu plan destroy service.""" + command = ["run", "service", "opentofu-plan", "destroy", name, "--raw"] + + for key, value in kwargs.items(): + if value is not None: + command.append(f"--{key}={value}") + + return await self.execute_command(command, timeout=600) + + # database operations + async def export_database(self, format: str = "yaml") -> Dict[str, Any]: + """export services and resources to a file.""" + raw_output = await self.execute_command(["db", "export", "--format", format, "--raw"], timeout=60) + + if format == "yaml": + # return raw yaml string + return {"data": raw_output if isinstance(raw_output, str) else json.dumps(raw_output), "format": "yaml"} + else: + return raw_output + + async def import_database( + self, + file_path: str, + repository: Optional[str] = None, + reference: Optional[str] = None, + private_key: Optional[str] = None, + force: bool = False, + check: bool = False, + validate_only: bool = False + ) -> Dict[str, Any]: + """import resources/services from a file.""" + command = ["db", "import"] + + if repository: + command.extend(["--repository", repository]) + if reference: + command.extend(["--reference", reference]) + if private_key: + command.extend(["--private-key-name", private_key]) + + if force: + command.append("--force") + if check: + command.append("--check") + if validate_only: + command.append("--validate") + + command.append(file_path) + command.append("--raw") + + try: + return await self.execute_command(command, timeout=120) + except ToreroExecutorError as e: + # import might return non-zero for conflicts but still have useful output + # handle this case gracefully + return { + "success": False, + "message": str(e) + } \ No newline at end of file From 2cc7d037705574350e09e7c57b72694927503049 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:16:07 -0400 Subject: [PATCH 09/13] Add `db sync` for services --- .../management/commands/sync_services.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 opt/torero-ui/torero_ui/dashboard/management/commands/sync_services.py diff --git a/opt/torero-ui/torero_ui/dashboard/management/commands/sync_services.py b/opt/torero-ui/torero_ui/dashboard/management/commands/sync_services.py new file mode 100644 index 0000000..3aeb100 --- /dev/null +++ b/opt/torero-ui/torero_ui/dashboard/management/commands/sync_services.py @@ -0,0 +1,65 @@ +"""Django management command to periodically sync services from torero cli.""" + +import logging +import time +from django.core.management.base import BaseCommand +from torero_ui.dashboard.services import DataCollectionService + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """Periodically sync services from torero cli to detect changes.""" + + help = 'Sync services from torero cli including those added via db import' + + def add_arguments(self, parser): + parser.add_argument( + '--once', + action='store_true', + help='Run sync once and exit' + ) + parser.add_argument( + '--interval', + type=int, + default=30, + help='Sync interval in seconds (default: 30)' + ) + + def handle(self, *args, **options): + once = options['once'] + interval = options['interval'] + + if once: + self._sync_once() + else: + self._sync_continuous(interval) + + def _sync_once(self): + """run sync once and exit.""" + try: + self.stdout.write("syncing services from torero cli...") + service = DataCollectionService() + service.sync_services() + self.stdout.write(self.style.SUCCESS("✅ services synchronized successfully")) + except Exception as e: + logger.error(f"failed to sync services: {e}") + self.stdout.write(self.style.ERROR(f"❌ sync failed: {e}")) + + def _sync_continuous(self, interval: int): + """continuously sync services at specified interval.""" + self.stdout.write(f"starting continuous sync every {interval} seconds...") + + while True: + try: + service = DataCollectionService() + service.sync_services() + logger.info("services synchronized successfully") + + except KeyboardInterrupt: + self.stdout.write("\nstopping service sync...") + break + except Exception as e: + logger.error(f"sync error: {e}") + + time.sleep(interval) \ No newline at end of file From c933f046d300e97a5628aa308f4a6bc0e010405c Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:16:33 -0400 Subject: [PATCH 10/13] Update tests.md --- docs/tests.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index 9c31992..4c098b6 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -10,11 +10,13 @@ This guide provides step-by-step instructions for running tests across the torer - [Common Issues](#common-issues) ## Prerequisites -Before running tests, ensure you have the following installed: +Before running tests locally, ensure you have the following installed: - Python 3.10 or higher -- `uv` package manager +- `uv` package manager (for local development) - Git (for version control) +**Note**: The examples below use `uv` for local development. When running tests inside the container, use the regular `pytest` commands as `uv` is not installed in the container. + ## Running All Tests ### Quick Start @@ -83,6 +85,24 @@ uv run pytest opt/torero-mcp/tests/ -k "test_health" uv run pytest opt/torero-mcp/tests/ -vv ``` +## Testing in Container + +For testing with torero CLI available (recommended): + +```bash +# Start the container +docker compose -f docker-compose.dev.yml up -d + +# Run MCP tests +docker compose -f docker-compose.dev.yml exec torero pytest opt/torero-mcp/tests/ -v + +# Run UI tests +docker compose -f docker-compose.dev.yml exec torero python opt/torero-ui/torero_ui/manage.py test + +# With coverage +docker compose -f docker-compose.dev.yml exec torero pytest opt/torero-mcp/tests/ --cov=opt/torero-mcp/torero_mcp --cov-report=term-missing +``` + ## Testing torero UI ### Step-by-Step Setup @@ -180,8 +200,8 @@ chmod +x tools.sh **Solution**: Tests that use ToreroExecutor or ToreroCliClient require torero to be installed ```bash -# Install torero or run tests in container -docker compose -f docker-compose.dev.yml exec torero uv run pytest +# Run tests in container (where torero is available) +docker compose -f docker-compose.dev.yml exec torero pytest opt/torero-mcp/tests/ ``` ## Advanced Testing From 3b321d385f35aa754742eda6381c4a634bfb40f7 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:17:52 -0400 Subject: [PATCH 11/13] Update README.md --- README.md | 58 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bc03e36..eb2b753 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Container image for [torero](https://torero.dev), built using vendor-neutral Con - Includes _torero_ installed and ready to go - Optional [OpenTofu](https://opentofu.org/) installation at runtime - Optional SSH administration for testing convenience + labs -- Integrated torero-api service for REST API access -- Integrated torero-mcp service for Model Context Protocol server +- Integrated torero-mcp service for Model Context Protocol server with direct CLI integration +- Optional torero-ui service for web-based dashboard ## Inspiration Managing and automating a hybrid, _multi-vendor_ infrastrcuture that encompasses _on-premises systems, private and public clouds, edge computing, and colocation environments_ poses significant challenges. How can you experiment to _learn_ without breaking things? How can you test new and innovative products like _torero_ on the test bench without friction to help in your evaluation? How do you test the behavior of changes in lower level environments before making changes to production? I use [containerlab](https://containerlab.dev/) for all of the above! This project makes it easy to insert _torero_ in your _containerlab_ topology file, connect to the container, and run your experiments -- the sky is the limit! @@ -28,7 +28,7 @@ docker run -d -p 2222:22 ghcr.io/torerodev/torero-container:latest ![docker cli](./img/docker-cli.gif) -### docker compose _(with custom OpenTofu version)_ +### docker compose _(with MCP server enabled)_ ```yaml --- services: @@ -37,14 +37,13 @@ services: container_name: torero ports: - "22:22" # use when ENABLE_SSH_ADMIN=true - - "8000:8000" # use when ENABLE_API=true - "8080:8080" # use when ENABLE_MCP=true + - "8001:8001" # use when ENABLE_UI=true volumes: - ./data:/home/admin/data environment: - - ENABLE_API=true # enable and run torero api - - API_PORT=8000 # api port - ENABLE_MCP=true # enable torero-mcp server + - ENABLE_UI=true # enable torero web dashboard - ENABLE_SSH_ADMIN=true # enable ssh admin at runtime - OPENTOFU_VERSION=1.9.0 # override OpenTofu version at runtime (optional) - PYTHON_VERSION=3.13.0 @@ -72,28 +71,24 @@ The following environment variables can be set at runtime: | Variable | Default | Description | |----------|---------|-------------| -| `ENABLE_API` | `false` | Enable torero API | -| `API_PORT` | `8000` | Set API port | -| `ENABLE_UI` | `false` | Enable torero UI | -| `UI_PORT` | `8001` | Set UI port | -| `ENABLE_MCP` | `false` | Enable torero MCP server | -| `ENABLE_SSH_ADMIN` | `false` | Enable SSH admin user | -| `OPENTOFU_VERSION` | `1.10.5` | Override OpenTofu version | -| `PYTHON_VERSION` | `3.13.0` | Set Python version | +| `ENABLE_UI` | `false` | Enable torero web dashboard | +| `UI_PORT` | `8001` | Set UI port | +| `ENABLE_MCP` | `false` | Enable torero MCP server | +| `ENABLE_SSH_ADMIN` | `false` | Enable SSH admin user | +| `OPENTOFU_VERSION` | `1.10.5` | Override OpenTofu version | +| `PYTHON_VERSION` | `3.13.0` | Set Python version | #### MCP Server Environment Variables When `ENABLE_MCP=true`, the following additional environment variables are available: | Variable | Default | Description | |----------|---------|-------------| -| `TORERO_MCP_TRANSPORT_TYPE` | `sse` | MCP transport type | +| `TORERO_MCP_TRANSPORT_TYPE` | `sse` | MCP transport type (stdio, sse, streamable_http) | | `TORERO_MCP_TRANSPORT_HOST` | `0.0.0.0` | MCP server host | | `TORERO_MCP_TRANSPORT_PORT` | `8080` | MCP server port | | `TORERO_MCP_TRANSPORT_PATH` | `/sse` | SSE endpoint path | -| `TORERO_API_BASE_URL` | `http://localhost:8000` | torero API base URL | -| `TORERO_API_TIMEOUT` | `30` | API request timeout in seconds | +| `TORERO_CLI_TIMEOUT` | `30` | CLI command timeout in seconds | | `TORERO_LOG_LEVEL` | `INFO` | Logging level | -| `TORERO_MCP_PID_FILE` | `/tmp/torero-mcp.pid` | PID file location | | `TORERO_MCP_LOG_FILE` | `/home/admin/.torero-mcp.log` | Log file path | ### OpenTofu Version Management @@ -113,6 +108,28 @@ services: The container will automatically download and install the requested version at startup if it differs from the pre-installed version. If no `OPENTOFU_VERSION` is specified, it uses the pre-installed version (1.10.5). +## MCP Server Integration + +The torero-mcp service provides Model Context Protocol integration with direct CLI access for optimal performance. This allows AI assistants and other MCP clients to interact with torero services without the overhead of an intermediate REST API. + +### Key Features: +- **Direct CLI Integration**: MCP tools execute torero commands directly for optimal performance +- **Comprehensive Tool Coverage**: Full access to torero services, repositories, decorators, secrets, and execution capabilities +- **Multiple Transport Options**: Support for stdio, Server-Sent Events (SSE), and streamable HTTP +- **Automatic Tool Discovery**: Dynamic loading of MCP tools with proper parameter mapping + +### Connecting MCP Clients: +```bash +# For stdio transport (direct process communication) +torero-mcp run --transport stdio + +# For SSE transport (web-based clients) +torero-mcp run --transport sse --host 0.0.0.0 --port 8080 + +# For streamable HTTP transport +torero-mcp run --transport streamable_http --host 0.0.0.0 --port 8080 +``` + ## Local Development ### Using docker-compose for local testing @@ -150,6 +167,11 @@ docker compose -f docker-compose.dev.yml down -v ## Container Architecture This project uses vendor-neutral Containerfile specifications for maximum compatibility across container runtimes. The image is built and distributed through GitHub Container Registry (GHCR) for reliable access and version management. +The architecture has been simplified to use direct CLI integration for optimal performance: +- **torero CLI**: Core torero functionality accessed directly +- **torero-mcp**: MCP server providing direct CLI integration for AI assistants +- **torero-ui**: Optional web dashboard for execution monitoring (uses CLI wrapper data) + ### Multi-Architecture Support The container images support both AMD64 and ARM64 architectures. The appropriate architecture will be selected automatically based on your host system when pulling the image. From f53cae9938a426616934d9116b9e5556801fda26 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:18:08 -0400 Subject: [PATCH 12/13] Remove legacy api references --- docs/README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/README.md b/docs/README.md index 80ece5d..8134454 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,21 +3,33 @@ This directory contains documentation for the torero Container project. ## Documentation Structure - **[tests.md](./tests.md)** - Step-by-step guide for running local tests -- **[openapi.json](./openapi.json)** - OpenAPI specification for the torero API (JSON format) -- **[openapi.yaml](./openapi.yaml)** - OpenAPI specification for the torero API (YAML format) +- **[global-context.md](../config/global-context.md)** - Comprehensive architecture documentation ## Components -### torero API -RESTful API service that provides programmatic access to torero functionality. Located in `opt/torero-api/`. - ### torero MCP Model Context Protocol server for integration with AI assistants. Located in `opt/torero-mcp/`. +- Direct CLI integration for optimal performance +- FastMCP-based implementation +- Support for SSE, stdio, and streamable HTTP transports ### torero UI Web-based user interface for managing torero operations. Located in `opt/torero-ui/`. +- Django-based dashboard with auto-refresh +- Automatic service synchronization +- Direct CLI integration via ToreroCliClient + +## Architecture Highlights + +The torero-container has been optimized for performance and simplicity: +- **Direct CLI Integration**: Both MCP and UI services interact directly with the torero CLI +- **Automatic Synchronization**: UI automatically detects service changes from any source +- **Universal CLI Wrapper**: Captures all executions for comprehensive tracking +- **Supervisor Management**: All services managed through a single supervisor configuration ## Quick Links - [Running Tests](./tests.md) -- [API Documentation](./openapi.yaml) -- [Main README](../README.md) \ No newline at end of file +- [Architecture Documentation](../config/global-context.md) +- [Main README](../README.md) +- [MCP Service Documentation](../opt/torero-mcp/README.md) +- [UI Service Documentation](../opt/torero-ui/README.md) \ No newline at end of file From cd3fbbe8a38ee3e15fd4623c802c4ec9dc6afdc9 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 22 Aug 2025 17:20:13 -0400 Subject: [PATCH 13/13] Remove legacy `api` references --- opt/torero-mcp/README.md | 110 ++++++++++++++++++++++++++++----------- opt/torero-ui/README.md | 90 +++++++++++++++++++++++++------- 2 files changed, 153 insertions(+), 47 deletions(-) diff --git a/opt/torero-mcp/README.md b/opt/torero-mcp/README.md index 2e70311..af91b1c 100644 --- a/opt/torero-mcp/README.md +++ b/opt/torero-mcp/README.md @@ -1,14 +1,24 @@ # torero MCP Service -Model Context Protocol (MCP) server providing AI assistants with access to torero automation capabilities. +Model Context Protocol (MCP) server providing AI assistants with direct access to torero automation capabilities through CLI integration. ## Features +- **Direct CLI Integration**: Execute torero commands directly without API overhead - **Service Management**: List, search, and inspect torero services +- **Service Execution**: Execute Ansible playbooks, Python scripts, and OpenTofu plans - **Decorator Operations**: Access and manage service decorators - **Repository Integration**: Browse and interact with torero repositories -- **Health Monitoring**: Check torero API connectivity and status -- **Service Execution**: Execute Ansible playbooks, Python scripts, and OpenTofu plans +- **Secret Management**: List and inspect secret metadata (values not exposed for security) - **Database Import/Export**: Backup and migrate configurations between environments +- **Health Monitoring**: Check torero CLI availability and version + +## Architecture +The MCP server has been redesigned to use direct CLI integration for optimal performance: + +- **Before**: MCP Tools → HTTP Client → torero-api → subprocess → torero CLI +- **Now**: MCP Tools → Direct subprocess → torero CLI + +This eliminates the HTTP API layer, reducing latency and complexity while maintaining full functionality. ## Configuration @@ -21,42 +31,46 @@ The MCP service is configured via environment variables: | `TORERO_MCP_TRANSPORT_HOST` | `0.0.0.0` | MCP server host | | `TORERO_MCP_TRANSPORT_PORT` | `8080` | MCP server port | | `TORERO_MCP_TRANSPORT_PATH` | `/sse` | SSE endpoint path | -| `TORERO_API_BASE_URL` | `http://localhost:8000` | torero API base URL | -| `TORERO_API_TIMEOUT` | `30` | API request timeout in seconds | +| `TORERO_CLI_TIMEOUT` | `30` | CLI command timeout in seconds | | `TORERO_LOG_LEVEL` | `INFO` | Logging level | ## Available MCP Tools ### Service Tools - `list_services` - List all available torero services with filtering -- `describe_service` - Get detailed information about a specific service -- `execute_service` - Execute a torero service with parameters +- `get_service` - Get detailed information about a specific service +- `describe_service` - Get complete description of a service +- `list_service_types` - Get all available service types +- `list_service_tags` - Get all available service tags + +### Service Execution Tools +- `execute_ansible_playbook` - Execute an Ansible playbook service +- `execute_python_script` - Execute a Python script service +- `execute_opentofu_plan_apply` - Apply OpenTofu infrastructure changes +- `execute_opentofu_plan_destroy` - Destroy OpenTofu infrastructure resources ### Decorator Tools -- `list_decorators` - List all available decorators +- `list_decorators` - List all available decorators with filtering - `get_decorator` - Get details about a specific decorator +- `list_decorator_types` - Get all available decorator types ### Repository Tools -- `list_repositories` - List all configured repositories +- `list_repositories` - List all configured repositories with filtering - `get_repository` - Get details about a specific repository +- `list_repository_types` - Get all available repository types -### Registry Tools -- `list_registries` - List all configured registries -- `list_registry_packages` - List packages in a specific registry +### Secret Tools +- `list_secrets` - List all configured secrets (metadata only) +- `get_secret` - Get details about a specific secret (metadata only, values not exposed) +- `list_secret_types` - Get all available secret types ### Database Tools -- `export_database` - Export torero database to a backup file -- `import_database` - Import torero database from a backup file +- `export_database` - Export torero database to YAML or JSON format +- `import_database` - Import torero database from a file or repository -### Secret Tools -- `list_secrets` - List all configured secrets -- `get_secret` - Get details about a specific secret (metadata only) -- `create_secret` - Create a new secret -- `update_secret` - Update an existing secret -- `delete_secret` - Delete a secret - -### System Tools -- `check_health` - Check the health status of torero API +### Health Tools +- `health_check` - Check the health status of torero CLI +- `get_torero_version` - Get torero version information ## Usage Examples @@ -80,6 +94,12 @@ torero-mcp run --transport sse --host 0.0.0.0 --port 8080 torero-mcp run --transport stdio ``` +#### Streamable HTTP Transport +```bash +# start with streamable HTTP transport +torero-mcp run --transport streamable_http --host 0.0.0.0 --port 8080 +``` + ### Daemon Mode ```bash # start as daemon @@ -110,6 +130,15 @@ Add to your Claude Desktop configuration: } ``` +### AnythingLLM Configuration +```json +{ + "name": "torero", + "url": "http://localhost:8080/sse", + "description": "torero automation integration" +} +``` + ### Custom Integration ```python import httpx @@ -130,19 +159,25 @@ for event in client: ```bash # from the container root cd opt/torero-mcp -python -m torero_mcp run +uv run python -m torero_mcp run # with specific transport -python -m torero_mcp run --transport sse --port 8080 +uv run python -m torero_mcp run --transport sse --port 8080 ``` ### Testing Tools ```bash -# test connection to API -python -m torero_mcp test-connection +# test CLI connection +uv run python -m torero_mcp test-connection # list available tools -python -m torero_mcp list-tools +uv run python -m torero_mcp list-tools +``` + +### Installing for Development +```bash +cd opt/torero-mcp +uv pip install -e . ``` ## Tool Response Format @@ -166,4 +201,21 @@ Error responses: "data": null, "error": "Error description" } -``` \ No newline at end of file +``` + +## Performance Benefits + +The direct CLI integration provides several performance benefits: + +1. **Reduced Latency**: Eliminates HTTP request/response overhead +2. **Lower Resource Usage**: No HTTP client or API server overhead +3. **Simplified Error Handling**: Direct access to subprocess errors +4. **Better Reliability**: Fewer failure points in the execution chain +5. **Improved Security**: No network traffic for internal operations + +## Security Considerations + +- Secret values are never exposed through MCP tools for security +- All CLI commands are executed with proper timeout limits +- No external network access required for core functionality +- CLI wrapper still captures execution data for UI dashboard when enabled \ No newline at end of file diff --git a/opt/torero-ui/README.md b/opt/torero-ui/README.md index 865ea4f..32c10fc 100644 --- a/opt/torero-ui/README.md +++ b/opt/torero-ui/README.md @@ -1,15 +1,23 @@ # torero-ui -Web dashboard for the torero automation platform. Provides visibility into service executions, statistics, and operational health. +Web dashboard for the torero automation platform. Provides visibility into service executions, statistics, and operational health with automatic service synchronization. ## Features - Real-time dashboard with auto-refresh +- Automatic service synchronization from torero CLI - Service execution history and statistics - Responsive design with torero branding - SQLite database with JSON columns for execution data - RESTful API for data access -- Integration with torero-api +- Direct CLI integration for real-time data + +## Architecture + +The UI service uses direct CLI integration for optimal performance: +- **ToreroCliClient**: Executes torero commands directly via subprocess +- **Sync Service**: Automatically detects and syncs service changes +- **CLI Wrapper**: Captures all executions for display in dashboard ## Quick Start @@ -31,38 +39,60 @@ docker run -d -p 8001:8001 torero-ui:latest 1. Install dependencies: ```bash -pip install -e . +uv pip install -e . ``` 2. Set up database: ```bash -python torero_ui/manage.py migrate +uv run python torero_ui/manage.py migrate ``` 3. Create superuser (optional): ```bash -python torero_ui/manage.py createsuperuser +uv run python torero_ui/manage.py createsuperuser ``` 4. Run development server: ```bash -python torero_ui/manage.py runserver 8001 +uv run python torero_ui/manage.py runserver 8001 +``` + +5. Run sync service (in another terminal): +```bash +uv run python torero_ui/manage.py sync_services ``` -5. Access dashboard at http://localhost:8001 +6. Access dashboard at http://localhost:8001 ## Configuration Environment variables: -- `TORERO_API_BASE_URL`: Base URL for torero-api (default: http://localhost:8000) -- `TORERO_API_TIMEOUT`: API request timeout in seconds (default: 30) -- `DASHBOARD_REFRESH_INTERVAL`: Auto-refresh interval in seconds (default: 30) -- `DEBUG`: Enable debug mode (default: True) +- `TORERO_CLI_TIMEOUT`: CLI command timeout in seconds (default: 30) +- `UI_REFRESH_INTERVAL`: Dashboard auto-refresh interval in seconds (default: 30) +- `DEBUG`: Enable debug mode (default: False) - `SECRET_KEY`: Django secret key +- `CONTAINER_BUILD_MODE`: Set to true during container build only + +## Service Synchronization + +The UI includes an automatic sync service that: +- Polls torero CLI for service changes +- Updates the database with new/modified services +- Runs at the interval specified by `UI_REFRESH_INTERVAL` +- Detects services added via CLI, database import, or MCP operations + +To run the sync service manually: +```bash +# Sync once +uv run python torero_ui/manage.py sync_services --once + +# Continuous sync (default) +uv run python torero_ui/manage.py sync_services --interval 30 +``` ## Integration with torero-container -Add to your containerlab topology or docker-compose: +Add to your docker-compose: ```yaml services: @@ -73,18 +103,42 @@ services: ports: - "8001:8001" environment: - - TORERO_API_BASE_URL=http://torero-container:8000 + - ENABLE_UI=true + - UI_REFRESH_INTERVAL=30 + - TORERO_CLI_TIMEOUT=30 - DEBUG=False - depends_on: - - torero-container + volumes: + - ./data:/home/admin/data # Persistent database ``` ## API Endpoints -- `GET /api/data/`: Dashboard data (stats, services, executions) -- `POST /api/sync/`: Sync services from torero-api +- `GET /`: Main dashboard view +- `GET /api/services/`: List all services +- `GET /api/executions/`: List recent executions +- `POST /api/record-execution/`: Record new execution - `GET /api/execution//`: Execution details +- `POST /api/sync/`: Trigger manual service sync -## License +## Database + +The UI uses SQLite with persistent storage in `/home/admin/data/torero_ui.db`. The database schema includes: + +- **ServiceInfo**: Stores service metadata and statistics +- **ServiceExecution**: Stores execution history with full details + +## CLI Integration + +The UI directly integrates with the torero CLI through the `ToreroCliClient` class: +```python +from torero_ui.dashboard.services import ToreroCliClient + +client = ToreroCliClient() +services = client.get_services() # Direct CLI call +``` + +This eliminates the need for an intermediate API layer and ensures data is always current. + +## License Apache-2.0 \ No newline at end of file