diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..e8025a4db6df11cd44be640a79125de1a7b605e9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,54 @@ +# This file is relative to the build context (repo root) + +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +.venv +venv +ENV +.eggs +*.egg-info +dist +build + +# Testing/Dev +.pytest_cache +.coverage +htmlcov +.mypy_cache +.ruff_cache +.pyright + +# IDE +.vscode +.idea +*.swp +*.swo + +# Frontend source (built files are already in src/flow/ui/ui/) +app/frontend/node_modules +app/frontend/src +app/frontend/*.json +app/frontend/*.ts +app/frontend/*.js +app/frontend/*.md +app/frontend/.vite + +# Docs and deploy folder itself +docs +deploy + +# Local env files (pass via docker env instead) +.env +.env.* +!.env.example + +# Tests (not needed in production) +tests diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..978507fc7add50668c55711a73eabbdd120236da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +# Flow UI Container +# Production-ready deployment with uvicorn workers + +FROM python:3.11-slim AS base + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install uv for fast dependency management +RUN pip install --no-cache-dir uv + +# ------------------------------------------------------------------- +# Builder stage: install dependencies +# ------------------------------------------------------------------- +FROM base AS builder + +# Copy only dependency files first (better layer caching) +COPY pyproject.toml uv.lock ./ + +# Install dependencies to system (no venv needed in container) +RUN uv pip install --system . + +# ------------------------------------------------------------------- +# Final stage: copy app and run +# ------------------------------------------------------------------- +FROM base AS final + +# Copy installed packages from builder +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application source (includes pre-built frontend in src/flow/ui/ui/) +COPY src/ ./src/ + +# Install the app itself (editable, uses already-installed deps) +RUN uv pip install --system --no-deps -e . + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash flowuser +RUN mkdir -p /app/data && chown -R flowuser:flowuser /app +USER flowuser + +# Configuration +ENV PORT=7860 +ENV FLOW_DATA_DIR=/app/data +ENV UVICORN_WORKERS=2 + +# Expose the port +EXPOSE ${PORT} + +# Health check - matches the actual endpoint in main.py +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:${PORT}/api/health || exit 1 + +# Production uvicorn with multiple workers +# - workers: handle concurrent requests (CPU-bound, use 2-4 for most cases) +# - For I/O bound (which this is), uvicorn's async handles concurrency well +# - limit-concurrency prevents overload +CMD uvicorn flow.ui.main:app \ + --host 0.0.0.0 \ + --port ${PORT} \ + --workers ${UVICORN_WORKERS} \ + --limit-concurrency 100 \ + --timeout-keep-alive 30 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8c63a9b7cd133dfb0454004445f2d68b5fbbd570 --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +--- +title: Flow - Autonomous Coding Agent +emoji: πŸ”„ +colorFrom: blue +colorTo: purple +sdk: docker +app_port: 7860 +pinned: false +--- + +# Flow + +**Autonomous Coding Agent with a Polished CLI** + +Flow is a standalone coding agent that can read, write, and execute code autonomously. It features a clean CLI interface similar to Claude Code, with support for multiple agent runtime harnesses. + +## Features + +- **Autonomous Execution**: Flow doesn't just tell you what to doβ€”it does it. Write code, run tests, fix errors, iterate. +- **Rich CLI**: Interactive REPL with streaming output, tool call visualization, and syntax highlighting. +- **Pluggable Harnesses**: Swap out the underlying agent runtime (Microsoft Agent Framework, OpenAI Swarm, etc.) +- **Persistent Memory**: Remember patterns, decisions, and context across sessions. +- **Workspace Isolation**: Secure file operations within a sandboxed workspace. + +## Installation + +```bash +# Basic installation +pip install flow-agent + +# With Microsoft Agent Framework support (recommended) +pip install flow-agent[agent-framework] + +# With all optional features +pip install flow-agent[all] + +# Development installation +pip install flow-agent[dev] +``` + +## Quick Start + +### 1. Configure Azure OpenAI + +```bash +export AZURE_OPENAI_API_KEY="your-api-key" +export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +export AZURE_OPENAI_DEPLOYMENT="gpt-4o" +``` + +### 2. Initialize Flow + +```bash +flow init +``` + +### 3. Run a Task + +```bash +# Single task +flow run "Create a Python script that calculates fibonacci numbers" + +# Interactive mode +flow run -i +``` + +## CLI Commands + +```bash +flow run [TASK] # Run a task or start interactive mode +flow config # Show current configuration +flow init # Initialize Flow directories +flow --help # Show help +``` + +## Usage as a Library + +```python +import asyncio +from flow import FlowAgent + +async def main(): + agent = FlowAgent() + + # Run a task + response = await agent.run("Create a hello world script") + print(response) + + # Stream events + async for event in agent.run_stream("List files in the workspace"): + print(event.type, event.content) + + await agent.close() + +asyncio.run(main()) +``` + +## Configuration + +Flow can be configured via environment variables or a config file. + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `FLOW_HARNESS` | Agent harness to use | `agent-framework` | +| `FLOW_MODEL` | Model name | `gpt-4o` | +| `FLOW_WORKSPACE` | Workspace directory | `~/.flow/workspace` | +| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key | - | +| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint | - | +| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment | - | + +### Directory Structure + +``` +~/.flow/ +β”œβ”€β”€ workspace/ # Agent's working directory +β”œβ”€β”€ memory/ # Persistent memory storage +β”‚ β”œβ”€β”€ patterns/ # Reusable code patterns +β”‚ β”œβ”€β”€ projects/ # Per-project notes +β”‚ └── decisions/ # Architecture decisions +└── skills/ # Domain-specific expertise +``` + +## Architecture + +### Harness System + +Flow uses a harness abstraction to support multiple agent runtimes: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ FlowAgent β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ BaseHarness β”‚ (Abstract) +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ β”‚ +β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” +β”‚ Agent β”‚ β”‚ OpenAIβ”‚ +β”‚ Frmwk β”‚ β”‚ Swarm β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Currently supported: +- **MAFHarness**: Microsoft Agent Framework with Azure OpenAI + +Planned: +- LangChain +- Claude SDK + +### Tools + +Flow includes a comprehensive set of tools: + +| Tool | Description | +|------|-------------| +| `read_file` | Read file contents with line numbers | +| `write_file` | Write/edit files (full write, str_replace, insert) | +| `list_directory` | List directory contents | +| `grep_search` | Search for patterns in code | +| `bash_execute` | Run shell commands | +| `python_repl` | Execute Python code snippets | +| `memory` | Persistent memory operations | +| `think` | Structured reasoning | +| `task_done` | Report task completion | + +## Development + +```bash +# Clone the repository +git clone https://github.com/victordibia/flow +cd flow + +# Install development dependencies +pip install -e ".[dev]" + +# Run tests +pytest tests/ -v + +# Type checking +pyright src/ +mypy src/ + +# Linting +ruff check src/ +ruff format src/ +``` + +## License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..8ca49adf86f7f93ea41b28ea478a885d14d10781 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,180 @@ +[project] +name = "flow-agent" +version = "0.1.0" +description = "Autonomous coding agent with a polished CLI" +authors = [{ name = "Victor Dibia" }] +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Typing :: Typed", +] + +dependencies = [ + "pydantic>=2.0.0", + "pydantic-settings>=2.0.0", + "rich>=13.0.0", + "typer>=0.9.0", + "httpx>=0.25.0", + "python-dotenv>=1.0.0", + "agent-framework-core>=1.0.0b0", + "azure-identity>=1.15.0", + "pyyaml>=6.0.0", + # OpenTelemetry for experiments tracing + "opentelemetry-api>=1.20.0", + "opentelemetry-sdk>=1.20.0", + "opentelemetry-semantic-conventions>=0.41b0", + # Web UI dependencies + "fastapi>=0.109.0", + "uvicorn>=0.27.0", + "sqlmodel>=0.0.14", + "aiosqlite>=0.19.0", +] + +[project.optional-dependencies] +# Optional features +research = ["beautifulsoup4>=4.12.0", "html2text>=2024.2.26"] + +# Bundles +all = ["flow-agent[research]"] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "pytest-cov>=4.1.0", + "mypy>=1.8.0", + "pyright>=1.1.350", + "ruff>=0.2.0", + "pre-commit>=3.6.0", + "poethepoet>=0.24.0", +] + +[project.scripts] +flow = "flow.cli:main" + +[project.urls] +Homepage = "https://github.com/victordibia/flow" +Repository = "https://github.com/victordibia/flow" +Issues = "https://github.com/victordibia/flow/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/flow"] + +# ============================================================================ +# Type Checking - Strict +# ============================================================================ + +[tool.pyright] +include = ["src"] +exclude = ["**/tests/**", "**/.venv/**"] +typeCheckingMode = "strict" +pythonVersion = "3.10" +reportMissingTypeStubs = false +reportUnnecessaryIsInstance = false +# agent_framework is optional - ignore type issues in harness +reportUnknownMemberType = "warning" +reportUnknownVariableType = "warning" +reportUnknownArgumentType = "warning" + +[tool.mypy] +plugins = ["pydantic.mypy"] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +# ============================================================================ +# Linting - Ruff +# ============================================================================ + +[tool.ruff] +line-length = 120 +target-version = "py310" +src = ["src"] +fix = true +include = ["*.py", "*.pyi", "**/pyproject.toml"] +exclude = ["docs/*"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "B", # bugbear + "UP", # pyupgrade + "ANN", # annotations + "S", # bandit (security) + "RUF", # ruff-specific + "ASYNC", # async checks + "D", # pydocstyle +] +ignore = [ + "D100", # allow missing docstring in public module + "D104", # allow missing docstring in public package + "D107", # allow missing docstring in __init__ + "ANN401", # allow Any type (needed for generic tool/event handling) + "S101", # allow assert statements (used in tests) +] + +[tool.ruff.lint.per-file-ignores] +"**/tests/**" = ["D", "ANN", "S"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +docstring-code-format = true + +# ============================================================================ +# Testing - Pytest +# ============================================================================ + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] + +[tool.coverage.run] +source = ["src/flow"] +omit = ["**/__init__.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] + +# ============================================================================ +# Task Runner - Poe +# ============================================================================ + +[tool.poe.tasks] +fmt = "ruff format src tests" +lint = "ruff check src tests --fix" +pyright = "pyright src" +mypy = "mypy src" +test = "pytest tests -v --cov=flow --cov-report=term-missing" +check = ["fmt", "lint", "pyright", "mypy", "test"] diff --git a/src/flow/__init__.py b/src/flow/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f1413cc79c538417fe430cbd394608f3d9183ece --- /dev/null +++ b/src/flow/__init__.py @@ -0,0 +1,26 @@ +"""Flow - Autonomous Coding Agent. + +An autonomous coding agent with a polished CLI experience. +Uses Microsoft Agent Framework as the runtime. + +Usage: + from flow.harness.maf import MAFHarness + + # Simple - creates agent with defaults + harness = MAFHarness() + async for event in harness.run_stream("Create a hello world script"): + print(event) + + # Or with custom settings + harness = MAFHarness(workspace=Path("/tmp/workspace"), enable_compaction=False) +""" + +from flow.harness.maf import MAFHarness, create_agent + +__version__ = "0.1.0" + +__all__ = [ + "MAFHarness", + "create_agent", + "__version__", +] diff --git a/src/flow/cli/__init__.py b/src/flow/cli/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e0780c8566f84a77ee43f0292a94fa94cde931f9 --- /dev/null +++ b/src/flow/cli/__init__.py @@ -0,0 +1,11 @@ +"""Flow CLI - Command-line interface. + +Provides the `flow` command for running the autonomous coding agent. +""" + +from flow.cli.app import app, main + +__all__ = [ + "app", + "main", +] diff --git a/src/flow/cli/app.py b/src/flow/cli/app.py new file mode 100644 index 0000000000000000000000000000000000000000..2587f64a69735f14076f5dd86b0a1396feff647c --- /dev/null +++ b/src/flow/cli/app.py @@ -0,0 +1,216 @@ +"""Flow CLI application. + +Main entry point for the `flow` command. +""" + +from __future__ import annotations + +import asyncio +import os +from pathlib import Path +from typing import Annotated + +import typer +from rich.console import Console + +from flow import __version__ + +app = typer.Typer( + name="flow", + help="Flow - Autonomous Coding Agent", + add_completion=False, + no_args_is_help=True, +) + +console = Console() + +# Default paths +DEFAULT_WORKSPACE = Path.home() / ".flow" / "workspace" +DEFAULT_MEMORY_PATH = Path.home() / ".flow" / "memory" + + +def version_callback(value: bool) -> None: + """Print version and exit.""" + if value: + console.print(f"Flow v{__version__}") + raise typer.Exit() + + +@app.callback() +def callback( + version: Annotated[ + bool | None, + typer.Option("--version", "-v", callback=version_callback, is_eager=True), + ] = None, +) -> None: + """Flow - Autonomous Coding Agent.""" + pass + + +@app.command() +def run( + task: Annotated[ + str | None, + typer.Argument(help="Task to execute (or enter interactive mode if not provided)"), + ] = None, + workspace: Annotated[ + Path | None, + typer.Option("--workspace", "-w", help="Workspace directory for writing files"), + ] = None, + config: Annotated[ + Path | None, + typer.Option("--config", "-c", help="Config file from optimization (YAML)"), + ] = None, + interactive: Annotated[ + bool, + typer.Option("--interactive/--no-interactive", "-i", help="Interactive mode"), + ] = True, +) -> None: + """Run the coding agent. + + If a task is provided, execute it and exit. + Otherwise, start an interactive REPL session. + + The agent can read files from anywhere but writes go to the workspace. + + Use --config to load a configuration from a previous optimization run. + """ + workspace_path = workspace or DEFAULT_WORKSPACE + memory_path = DEFAULT_MEMORY_PATH + + # Ensure directories exist + workspace_path.mkdir(parents=True, exist_ok=True) + memory_path.mkdir(parents=True, exist_ok=True) + + if task: + # Single task mode + asyncio.run(_run_single_task(workspace_path, memory_path, task, config)) + elif interactive: + # Interactive REPL mode + from flow.cli.repl import FlowREPL + repl = FlowREPL(workspace=workspace_path, memory_path=memory_path) + asyncio.run(repl.run()) + else: + console.print("[red]Error:[/] No task provided and interactive mode disabled.") + raise typer.Exit(1) + + +async def _run_single_task( + workspace: Path, + memory_path: Path, + task: str, + config_path: Path | None = None, +) -> None: + """Run a single task and print the result.""" + from flow.cli.output import print_event + from flow.harness.base import EventType + from flow.harness.maf import MAFHarness + + if config_path: + # Load config from optimization result + from flow.experiments.config_export import load_config + from flow.experiments.ablation import create_harness_from_config + + ablation_config = load_config(config_path) + console.print(f"[dim]Using config: {ablation_config.name}[/]") + harness = create_harness_from_config(ablation_config, workspace) + else: + harness = MAFHarness(workspace=workspace, memory_path=memory_path) + + try: + console.print("\n[bold blue]Flow[/] - Executing task...\n") + + async for event in harness.run_stream(task): + print_event(console, event) + + if event.type == EventType.ERROR: + raise typer.Exit(1) + + except KeyboardInterrupt: + console.print("\n[yellow]Cancelled.[/]") + finally: + await harness.close() + + +# Import and register the optimize command +from flow.cli.optimize import optimize as optimize_cmd + +app.command()(optimize_cmd) + + +@app.command() +def serve( + host: Annotated[ + str, + typer.Option("--host", "-h", help="Host to bind to"), + ] = "0.0.0.0", # noqa: S104 + port: Annotated[ + int, + typer.Option("--port", "-p", help="Port to bind to"), + ] = 8091, + reload: Annotated[ + bool, + typer.Option("--reload", help="Enable auto-reload for development"), + ] = False, +) -> None: + """Start the Flow web UI server. + + Launches a web interface for managing agent configurations, + running optimization experiments, and viewing results. + """ + import uvicorn + + console.print(f"\n[bold blue]Flow UI[/] starting on [cyan]http://{host}:{port}[/]\n") + + uvicorn.run( + "flow.ui.main:app", + host=host, + port=port, + reload=reload, + ) + + +@app.command() +def config() -> None: + """Show current configuration.""" + from rich.table import Table + + table = Table(title="Flow Configuration") + table.add_column("Setting", style="cyan") + table.add_column("Value", style="green") + + table.add_row("Workspace", str(DEFAULT_WORKSPACE)) + table.add_row("Memory Path", str(DEFAULT_MEMORY_PATH)) + table.add_row("Azure Endpoint", os.environ.get("AZURE_OPENAI_ENDPOINT", "(not set)")) + table.add_row("Azure Deployment", os.environ.get("AZURE_OPENAI_DEPLOYMENT", "(not set)")) + + console.print(table) + + +@app.command() +def init() -> None: + """Initialize Flow directories and show setup instructions.""" + DEFAULT_WORKSPACE.mkdir(parents=True, exist_ok=True) + DEFAULT_MEMORY_PATH.mkdir(parents=True, exist_ok=True) + + console.print("\n[bold green]Flow initialized![/]\n") + console.print(f" Workspace: [cyan]{DEFAULT_WORKSPACE}[/]") + console.print(f" Memory: [cyan]{DEFAULT_MEMORY_PATH}[/]") + + console.print("\n[bold]Next steps:[/]") + console.print(" 1. Set your Azure OpenAI credentials:") + console.print(" [dim]export AZURE_OPENAI_API_KEY=your-key[/]") + console.print(" [dim]export AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/[/]") + console.print(" [dim]export AZURE_OPENAI_DEPLOYMENT=your-deployment[/]") + console.print("\n 2. Run Flow:") + console.print(' [dim]flow run "Create a hello world Python script"[/]') + console.print(" [dim]flow run -i # Interactive mode[/]") + + +def main() -> None: + """Main entry point.""" + app() + + +if __name__ == "__main__": + main() diff --git a/src/flow/cli/optimize.py b/src/flow/cli/optimize.py new file mode 100644 index 0000000000000000000000000000000000000000..0410275c53a89f592c099b4fadab262f5ac70ae8 --- /dev/null +++ b/src/flow/cli/optimize.py @@ -0,0 +1,332 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Optimize command for finding best agent configurations.""" + +from __future__ import annotations + +import asyncio +import importlib.util +import sys +from pathlib import Path +from typing import Annotated, Any + +import typer +from rich.console import Console + +from flow.experiments.ablation import AblationConfig, CONTEXT_ENGINEERING_CONFIGS +from flow.experiments.optimizer import ( + FlowOptimizer, + generate_grid_configs, + load_tasks_from_jsonl, +) +from flow.experiments.types import EvalCriterion, Task + +console = Console() + + +def optimize( + tasks: Annotated[ + Path | None, + typer.Option( + "--tasks", "-t", + help="Path to tasks.jsonl file", + ), + ] = None, + config: Annotated[ + Path | None, + typer.Option( + "--config", "-c", + help="Path to Python config file with CONFIGS or VARIATIONS", + ), + ] = None, + agent: Annotated[ + Path | None, + typer.Option( + "--agent", "-a", + help="Path to base agent Python file (for optimization)", + ), + ] = None, + suite: Annotated[ + str | None, + typer.Option( + "--suite", "-s", + help="Built-in task suite: coding, research", + ), + ] = None, + parallel: Annotated[ + int, + typer.Option( + "--parallel", "-p", + help="Max concurrent experiments", + ), + ] = 4, + mode: Annotated[ + str, + typer.Option( + "--mode", "-m", + help="Config mode: named (use CONFIGS), grid (use VARIATIONS)", + ), + ] = "named", + vary: Annotated[ + str | None, + typer.Option( + "--vary", "-v", + help="Comma-separated params to vary: compaction,memory,model", + ), + ] = None, + output: Annotated[ + Path | None, + typer.Option( + "--output", "-o", + help="Output directory for results", + ), + ] = None, + no_llm_eval: Annotated[ + bool, + typer.Option( + "--no-llm-eval", + help="Disable LLM-as-Judge evaluation (faster, less accurate)", + ), + ] = False, +) -> None: + """Find the best agent configuration through experimentation. + + Runs experiments in parallel, evaluates with LLM-as-Judge, + ranks via Pareto analysis, and exports winning configs. + + Examples: + + # Run with task file and default configs + flow optimize --tasks tasks.jsonl + + # Use custom configs from Python file + flow optimize --config my_configs.py --tasks tasks.jsonl + + # Grid search over variations + flow optimize --config my_configs.py --tasks tasks.jsonl --mode grid + + # Use built-in task suite + flow optimize --suite coding --parallel 2 + + # Vary specific parameters + flow optimize --vary compaction,memory --tasks tasks.jsonl + """ + asyncio.run(_run_optimize( + tasks_path=tasks, + config_path=config, + agent_path=agent, + suite=suite, + parallel=parallel, + mode=mode, + vary=vary, + output_dir=output, + use_llm_eval=not no_llm_eval, + )) + + +async def _run_optimize( + tasks_path: Path | None, + config_path: Path | None, + agent_path: Path | None, + suite: str | None, + parallel: int, + mode: str, + vary: str | None, + output_dir: Path | None, + use_llm_eval: bool, +) -> None: + """Run the optimization.""" + # Load tasks + tasks = _load_tasks(tasks_path, suite) + if not tasks: + console.print("[red]Error:[/] No tasks specified. Use --tasks or --suite") + raise typer.Exit(1) + + # Load configs + configs = _load_configs(config_path, mode, vary) + if not configs: + console.print("[red]Error:[/] No configs to test. Use --config or --vary") + raise typer.Exit(1) + + console.print(f"\n[bold]Tasks:[/] {len(tasks)}") + for t in tasks: + console.print(f" - {t.name}") + + console.print(f"\n[bold]Configs:[/] {len(configs)}") + for c in configs: + console.print(f" - {c.name}") + + # Run optimizer + optimizer = FlowOptimizer( + parallel=parallel, + use_llm_evaluator=use_llm_eval, + output_dir=output_dir, + ) + + try: + result = await optimizer.optimize(configs, tasks) + + console.print("\n[bold green]Optimization complete![/]") + console.print(f"\nBest configs exported to: [cyan]{result.output_dir / 'configs'}[/]") + console.print("\nTo use a config:") + console.print(f" [dim]flow run --config {result.output_dir / 'configs' / 'best_score.yaml'} \"your task\"[/]") + + except KeyboardInterrupt: + console.print("\n[yellow]Optimization cancelled.[/]") + raise typer.Exit(1) + + +def _load_tasks(tasks_path: Path | None, suite: str | None) -> list[Task]: + """Load tasks from file or built-in suite.""" + if tasks_path: + if not tasks_path.exists(): + console.print(f"[red]Error:[/] Tasks file not found: {tasks_path}") + raise typer.Exit(1) + return load_tasks_from_jsonl(tasks_path) + + if suite: + return _get_builtin_suite(suite) + + # Default: simple test suite + return _get_builtin_suite("quick") + + +def _get_builtin_suite(name: str) -> list[Task]: + """Get a built-in task suite.""" + suites = { + "quick": [ + Task( + name="hello_world", + prompt="Create a Python script 'hello.py' that prints 'Hello, World!' and run it.", + criteria=[ + EvalCriterion(name="file_created", instruction="hello.py should be created"), + EvalCriterion(name="correct_output", instruction="Output should include 'Hello, World!'"), + ], + ), + ], + "coding": [ + Task( + name="fizzbuzz", + prompt="Create fizzbuzz.py that prints 1-30 with Fizz/Buzz/FizzBuzz rules. Run it.", + criteria=[ + EvalCriterion(name="file_created", instruction="fizzbuzz.py should be created"), + EvalCriterion(name="correct_output", instruction="Output shows correct FizzBuzz pattern"), + ], + metadata={"category": "short"}, + ), + Task( + name="rest_api", + prompt="Create a FastAPI app with a /health endpoint that returns JSON {'status': 'ok'}. Save as api.py.", + criteria=[ + EvalCriterion(name="file_created", instruction="api.py should be created"), + EvalCriterion(name="fastapi_used", instruction="Should use FastAPI"), + EvalCriterion(name="endpoint_defined", instruction="Should have /health endpoint"), + ], + metadata={"category": "medium"}, + ), + Task( + name="data_pipeline", + prompt="""Create a data processing pipeline: +1. data_types.py - DataRecord dataclass (id, name, value) +2. validators.py - validate_id, validate_name functions +3. pipeline.py - chain validators together +4. test_pipeline.py - tests for the pipeline +Run the tests.""", + criteria=[ + EvalCriterion(name="modules_created", instruction="All 4 Python files created"), + EvalCriterion(name="tests_run", instruction="Tests should be executed"), + ], + metadata={"category": "long"}, + ), + ], + "research": [ + Task( + name="codebase_analysis", + prompt="""Analyze this workspace: +1. Explore the directory structure +2. Identify Python files and their purposes +3. Create analysis_report.md with findings""", + criteria=[ + EvalCriterion(name="exploration", instruction="Should explore directory"), + EvalCriterion(name="report_created", instruction="analysis_report.md created"), + ], + metadata={"category": "research"}, + ), + ], + } + + if name not in suites: + console.print(f"[red]Error:[/] Unknown suite '{name}'. Available: {list(suites.keys())}") + raise typer.Exit(1) + + return suites[name] + + +def _load_configs( + config_path: Path | None, + mode: str, + vary: str | None, +) -> list[AblationConfig]: + """Load configs from file or generate from variations.""" + # Load from Python file + if config_path: + if not config_path.exists(): + console.print(f"[red]Error:[/] Config file not found: {config_path}") + raise typer.Exit(1) + + configs, variations = _load_python_config(config_path) + + if mode == "grid" and variations: + return generate_grid_configs("grid", variations) + elif configs: + return configs + else: + console.print("[red]Error:[/] Config file has no CONFIGS or VARIATIONS") + raise typer.Exit(1) + + # Generate from --vary flag + if vary: + variations = _parse_vary_flag(vary) + return generate_grid_configs("vary", variations) + + # Default: use context engineering configs + return CONTEXT_ENGINEERING_CONFIGS + + +def _load_python_config(path: Path) -> tuple[list[AblationConfig], dict[str, Any]]: + """Load CONFIGS and VARIATIONS from a Python file.""" + spec = importlib.util.spec_from_file_location("config_module", path) + if spec is None or spec.loader is None: + raise ValueError(f"Cannot load {path}") + + module = importlib.util.module_from_spec(spec) + sys.modules["config_module"] = module + spec.loader.exec_module(module) + + configs = getattr(module, "CONFIGS", []) + variations = getattr(module, "VARIATIONS", {}) + + return configs, variations + + +def _parse_vary_flag(vary: str) -> dict[str, Any]: + """Parse --vary flag into variations dict.""" + variations = {} + + for param in vary.split(","): + param = param.strip().lower() + + if param in ("compaction", "compact"): + variations["enable_message_compaction"] = [True, False] + elif param in ("memory", "mem"): + variations["enable_memory_tool"] = [True, False] + elif param in ("subagent", "sub"): + variations["enable_sub_agent"] = [True, False] + elif param in ("head", "head_size"): + variations["compaction_head_size"] = [5, 10, 20] + elif param in ("tail", "tail_size"): + variations["compaction_tail_size"] = [20, 40, 60] + else: + console.print(f"[yellow]Warning:[/] Unknown vary param: {param}") + + return variations diff --git a/src/flow/cli/output.py b/src/flow/cli/output.py new file mode 100644 index 0000000000000000000000000000000000000000..742ae71b6df57a65b3eaedddcfda6869f382deca --- /dev/null +++ b/src/flow/cli/output.py @@ -0,0 +1,99 @@ +"""Output formatting for Flow CLI. + +Provides functions for rendering agent events to the terminal +with rich formatting. +""" + +from rich.console import Console +from rich.markdown import Markdown +from rich.markup import escape +from rich.panel import Panel +from rich.syntax import Syntax + +from flow.harness.base import Event, EventType + + +def print_event(console: Console, event: Event) -> None: + """Print an agent event to the console. + + Args: + console: Rich console instance + event: Event to print + """ + if event.type == EventType.TEXT_DELTA: + # Stream text without newline + console.print(event.content, end="") + + elif event.type == EventType.TEXT_DONE: + # Final text - print with newline + if event.content: + console.print(event.content) + console.print() # Extra newline for spacing + + elif event.type == EventType.TOOL_CALL_START: + # Show tool being called + tool_name = event.tool_name or "unknown" + console.print(f"\n[dim]β–Ά Calling tool:[/] [cyan]{tool_name}[/]") + + elif event.type == EventType.TOOL_CALL_ARGS: + # Show tool arguments (streaming) - escape to prevent Rich markup interpretation + if event.content: + console.print(f"[dim]{escape(event.content)}[/]", end="") + + elif event.type == EventType.TOOL_CALL_DONE: + # Tool call complete + console.print() # Newline after args + + elif event.type == EventType.TOOL_RESULT: + # Show tool result (truncated if long) + result = event.content or "" + if len(result) > 500: + result = result[:500] + "\n... (truncated)" + + console.print(Panel( + escape(result), + title="[green]Tool Result[/]", + border_style="dim", + expand=False, + )) + + elif event.type == EventType.THINKING: + # Show agent thinking + console.print(f"[dim italic]πŸ’­ {escape(event.content or '')}[/]") + + elif event.type == EventType.ERROR: + # Show error + console.print(f"\n[bold red]Error:[/] {escape(event.content or '')}") + + elif event.type == EventType.DONE: + # Execution complete + console.print("\n[dim]─── Done ───[/]\n") + + +def print_welcome(console: Console) -> None: + """Print welcome message for interactive mode.""" + console.print("\n[bold blue]Flow[/] - Autonomous Coding Agent") + console.print("[dim]Type your task and press Enter. Type 'exit' or Ctrl+D to quit.[/]\n") + + +def print_code(console: Console, code: str, language: str = "python") -> None: + """Print syntax-highlighted code. + + Args: + console: Rich console instance + code: Code to print + language: Programming language for syntax highlighting + """ + syntax = Syntax(code, language, theme="monokai", line_numbers=True) + console.print(syntax) + + +def print_markdown(console: Console, text: str) -> None: + """Print markdown-formatted text. + + Args: + console: Rich console instance + text: Markdown text to print + """ + md = Markdown(text) + console.print(md) diff --git a/src/flow/cli/repl.py b/src/flow/cli/repl.py new file mode 100644 index 0000000000000000000000000000000000000000..c6f9301f8fa563feb70420c5437c7165ebf3045a --- /dev/null +++ b/src/flow/cli/repl.py @@ -0,0 +1,153 @@ +"""Interactive REPL for Flow. + +Provides an interactive command-line interface for running +the Flow agent with streaming output. +""" + +from __future__ import annotations + +from pathlib import Path + +from rich.console import Console + +from flow.cli.output import print_event, print_welcome +from flow.harness.base import EventType +from flow.harness.maf import MAFHarness + +# Default paths +DEFAULT_WORKSPACE = Path.home() / ".flow" / "workspace" +DEFAULT_MEMORY_PATH = Path.home() / ".flow" / "memory" + + +class FlowREPL: + """Interactive REPL for Flow agent. + + Provides a command-line interface similar to Claude Code, + with streaming output and tool call visualization. + """ + + def __init__( + self, + workspace: Path | None = None, + memory_path: Path | None = None, + ) -> None: + """Initialize the REPL. + + Args: + workspace: Workspace directory. Defaults to ~/.flow/workspace. + memory_path: Memory directory. Defaults to ~/.flow/memory. + """ + self._workspace = workspace or DEFAULT_WORKSPACE + self._memory_path = memory_path or DEFAULT_MEMORY_PATH + self._console = Console() + self._harness: MAFHarness | None = None + self._thread_id: str | None = None + + def _get_harness(self) -> MAFHarness: + """Get or create the harness instance.""" + if self._harness is None: + self._harness = MAFHarness( + workspace=self._workspace, + memory_path=self._memory_path, + ) + return self._harness + + async def run(self) -> None: + """Run the interactive REPL loop.""" + print_welcome(self._console) + + harness = self._get_harness() + + while True: + try: + # Get user input + user_input = self._get_input() + + if user_input is None: + # EOF (Ctrl+D) + break + + user_input = user_input.strip() + + if not user_input: + continue + + # Handle special commands + if user_input.lower() in ("exit", "quit", "q"): + break + + if user_input.lower() == "clear": + self._console.clear() + print_welcome(self._console) + continue + + if user_input.lower() == "help": + self._print_help() + continue + + if user_input.lower() == "config": + self._print_config() + continue + + # Run the task + await self._run_task(harness, user_input) + + except KeyboardInterrupt: + self._console.print("\n[yellow]Interrupted. Type 'exit' to quit.[/]") + continue + + # Cleanup + self._console.print("\n[dim]Goodbye![/]\n") + if self._harness: + await self._harness.close() + + def _get_input(self) -> str | None: + """Get input from the user. + + Returns: + User input string, or None on EOF. + """ + try: + return self._console.input("[bold green]>[/] ") + except EOFError: + return None + + async def _run_task(self, harness: MAFHarness, task: str) -> None: + """Run a task and stream the output. + + Args: + harness: Harness instance + task: Task to execute + """ + self._console.print() # Blank line before output + + try: + async for event in harness.run_stream(task, self._thread_id): + print_event(self._console, event) + + # Store thread ID for conversation continuity + if event.type == EventType.DONE: + self._thread_id = harness.get_thread_id() + + except Exception as e: + self._console.print(f"\n[bold red]Error:[/] {e}") + + def _print_help(self) -> None: + """Print help information.""" + self._console.print("\n[bold]Flow Commands:[/]") + self._console.print(" [cyan]exit[/], [cyan]quit[/], [cyan]q[/] - Exit the REPL") + self._console.print(" [cyan]clear[/] - Clear the screen") + self._console.print(" [cyan]config[/] - Show current configuration") + self._console.print(" [cyan]help[/] - Show this help message") + self._console.print("\n[bold]Tips:[/]") + self._console.print(" - Type your task and press Enter to execute") + self._console.print(" - Press Ctrl+C to cancel a running task") + self._console.print(" - Press Ctrl+D to exit") + self._console.print() + + def _print_config(self) -> None: + """Print current configuration.""" + self._console.print("\n[bold]Configuration:[/]") + self._console.print(f" Workspace: [cyan]{self._workspace}[/]") + self._console.print(f" Memory: [cyan]{self._memory_path}[/]") + self._console.print() diff --git a/src/flow/experiments/__init__.py b/src/flow/experiments/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1dd2cb7aeb6e0b4ab35dd98f1381c474e26a0ee --- /dev/null +++ b/src/flow/experiments/__init__.py @@ -0,0 +1,204 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Experiments framework for running and evaluating Flow agent tasks. + +This package provides a structured way to: +- Define tasks with evaluation criteria +- Run agents on tasks and collect OpenTelemetry traces +- Evaluate agent outputs using LLM, heuristic, or trace-based evaluators +- Extract metrics from execution traces +- Run ablation studies comparing different configurations + +Example usage: + from flow.harness.maf import MAFHarness + from flow.experiments import ( + FlowExperimentRunner, + Task, + EvalCriterion, + TraceEvaluator, + HeuristicEvaluator, + extract_metrics, + format_metrics_summary, + setup_tracing, + ) + + # Setup tracing (call once at startup) + setup_tracing("my-experiment") + + # Define a task + task = Task( + name="hello_world", + prompt="Write a Python function that prints 'Hello, World!'", + criteria=[ + EvalCriterion( + name="correctness", + instruction="The function should print exactly 'Hello, World!'", + ), + ], + ) + + # Run the experiment + harness = MAFHarness() + runner = FlowExperimentRunner(keep_workspace=True) + result = await runner.run(harness, task) + + # Extract metrics + metrics = extract_metrics(result.trace) + print(format_metrics_summary(metrics)) + + # Evaluate the result + evaluator = HeuristicEvaluator() + eval_result = await evaluator.evaluate(result) + print(f"Score: {eval_result.score}, Passed: {eval_result.passed}") + + await harness.close() + +Ablation studies: + from flow.experiments import run_ablations, AblationConfig + + configs = [ + AblationConfig(name="baseline", enable_message_compaction=False), + AblationConfig(name="with_compaction", enable_message_compaction=True), + ] + + results = await run_ablations( + configs, + task_prompt="Create a simple HTTP server", + ) +""" + +# Types +# Ablation +from .ablation import ( + AGENT_MEMORY_ONLY, + ALL_CONTEXT_ENGINEERING, + COMPACTION_ONLY, + # Context engineering configs + CONTEXT_ENG_BASELINE, + CONTEXT_ENGINEERING_CONFIGS, + ISOLATION_ONLY, + AblationConfig, + AblationResult, + # Shared utilities + compute_pareto_frontier, + create_harness_from_config, + generate_recommendation, + run_ablations, + run_context_engineering_comparison, + run_single_ablation, +) + +# Config export +from .config_export import ( + export_config, + export_optimization_configs, + load_config, +) + +# Evaluators +from .evaluators import ( + CompositeEvaluator, + Evaluator, + HeuristicEvaluator, + LLMEvaluator, + TraceEvaluator, +) + +# Metrics +from .metrics import ( + LLMCallInfo, + ToolCallInfo, + TraceMetrics, + extract_metrics, + format_metrics_summary, + metrics_to_dict, +) + +# Optimizer +from .optimizer import ( + ConfigSummary, + FlowOptimizer, + OptimizationResult, + TaskResult, + generate_grid_configs, + load_tasks_from_jsonl, +) + +# Reporters +from .reporters import ( + load_run_result_summary, + print_comparison_table, + print_eval_result, + print_metrics_summary, + save_comparison, + save_run_result, +) + +# Runner +from .runner import FlowExperimentRunner, setup_tracing + +# Trace collection +from .trace_collector import FlowTraceCollector +from .types import CriterionResult, EvalCriterion, EvalResult, RunResult, Task + +__all__ = [ # noqa: RUF022 # Intentionally grouped by category + # Types + "Task", + "EvalCriterion", + "RunResult", + "EvalResult", + "CriterionResult", + # Trace collection + "FlowTraceCollector", + # Metrics + "TraceMetrics", + "LLMCallInfo", + "ToolCallInfo", + "extract_metrics", + "format_metrics_summary", + "metrics_to_dict", + # Runner + "FlowExperimentRunner", + "setup_tracing", + # Evaluators + "Evaluator", + "LLMEvaluator", + "TraceEvaluator", + "HeuristicEvaluator", + "CompositeEvaluator", + # Reporters + "save_run_result", + "load_run_result_summary", + "save_comparison", + "print_metrics_summary", + "print_comparison_table", + "print_eval_result", + # Ablation + "AblationConfig", + "AblationResult", + "run_ablations", + "run_single_ablation", + "create_harness_from_config", + # Context engineering configs + "CONTEXT_ENG_BASELINE", + "COMPACTION_ONLY", + "AGENT_MEMORY_ONLY", + "ISOLATION_ONLY", + "ALL_CONTEXT_ENGINEERING", + "CONTEXT_ENGINEERING_CONFIGS", + "run_context_engineering_comparison", + # Shared utilities + "compute_pareto_frontier", + "generate_recommendation", + # Optimizer + "FlowOptimizer", + "OptimizationResult", + "ConfigSummary", + "TaskResult", + "generate_grid_configs", + "load_tasks_from_jsonl", + # Config export + "export_config", + "load_config", + "export_optimization_configs", +] diff --git a/src/flow/experiments/ablation.py b/src/flow/experiments/ablation.py new file mode 100644 index 0000000000000000000000000000000000000000..408a2a47ffe4cfbbc1289c5689dc6811fe5a3813 --- /dev/null +++ b/src/flow/experiments/ablation.py @@ -0,0 +1,472 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Ablation runner for comparing Flow agent configurations. + +This module provides: +- AblationConfig: Dataclass for agent configuration parameters +- Pareto analysis utilities for multi-objective optimization +- Pre-defined configurations for context engineering strategies +- Convenience functions for running ablation studies +""" + +from __future__ import annotations + +import json +import logging +from dataclasses import asdict, dataclass +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING + +from .evaluators import HeuristicEvaluator +from .metrics import TraceMetrics, extract_metrics, metrics_to_dict +from .reporters import print_comparison_table, save_run_result +from .runner import FlowExperimentRunner, setup_tracing +from .types import EvalCriterion, RunResult, Task + +if TYPE_CHECKING: + from flow.harness.maf import MAFHarness + + from .optimizer import ConfigSummary + +logger = logging.getLogger(__name__) + + +@dataclass +class AblationConfig: + """Configuration for a single ablation run. + + Each config represents a different agent configuration to test. + The name is used as an identifier in comparison results. + + Attributes: + name: Unique identifier for this configuration + enable_message_compaction: Whether to enable message compaction + enable_memory_tool: Whether to enable agent-managed memory + enable_sub_agent: Whether to enable sub-agent for isolated research + compaction_head_size: Number of initial messages to keep + compaction_tail_size: Number of recent messages to keep + bash_timeout: Timeout for bash commands in seconds + """ + + name: str + enable_message_compaction: bool = True + enable_memory_tool: bool = True + enable_sub_agent: bool = False + compaction_head_size: int = 10 + compaction_tail_size: int = 40 + bash_timeout: int = 120 + + +@dataclass +class AblationResult: + """Result of a single ablation run. + + Contains all data from the run including raw results, + extracted metrics, and evaluation scores. + """ + + config: AblationConfig + run_result: RunResult + metrics: TraceMetrics + eval_score: float + eval_passed: bool + eval_reasoning: str + + +def create_harness_from_config(config: AblationConfig, workspace: Path) -> MAFHarness: + """Create a MAFHarness from an ablation config. + + Args: + config: The ablation configuration + workspace: Working directory + + Returns: + A configured MAFHarness + """ + from flow.harness.maf import MAFHarness + + return MAFHarness( + workspace=workspace, + memory_path=workspace / "memory", + enable_compaction=config.enable_message_compaction, + enable_memory_tool=config.enable_memory_tool, + enable_sub_agent=config.enable_sub_agent, + compaction_head_size=config.compaction_head_size, + compaction_tail_size=config.compaction_tail_size, + bash_timeout=config.bash_timeout, + ) + + +async def run_single_ablation( + config: AblationConfig, + task: Task, + workspace: Path, +) -> AblationResult: + """Run a single ablation with trace capture and evaluation. + + Args: + config: The ablation configuration + task: The task to run + workspace: Working directory + + Returns: + AblationResult with metrics and evaluation + """ + # Create harness from config + harness = create_harness_from_config(config, workspace) + + try: + # Create runner + runner = FlowExperimentRunner(keep_workspace=True) + + # Run the experiment + run_result = await runner.run(harness, task, workspace=workspace) + + # Extract metrics + metrics = extract_metrics(run_result.trace) + + # Evaluate the result + evaluator = HeuristicEvaluator() + eval_result = await evaluator.evaluate(run_result) + + return AblationResult( + config=config, + run_result=run_result, + metrics=metrics, + eval_score=eval_result.score, + eval_passed=eval_result.passed, + eval_reasoning=eval_result.reasoning, + ) + finally: + await harness.close() + + +def save_ablation_result(result: AblationResult, output_dir: Path) -> None: + """Save ablation result to files. + + Creates a subdirectory for the config with all result files. + + Args: + result: The ablation result to save + output_dir: Base directory for output + """ + config_dir = output_dir / result.config.name + save_run_result( + result.run_result, + config_dir, + metrics=result.metrics, + ) + + # Save ablation-specific data + with open(config_dir / "ablation.json", "w") as f: + json.dump({ + "config": asdict(result.config), + "evaluation": { + "score": result.eval_score, + "passed": result.eval_passed, + "reasoning": result.eval_reasoning, + }, + }, f, indent=2) + + +async def run_ablations( + configs: list[AblationConfig], + task_prompt: str, + output_dir: Path | None = None, + task_name: str = "ablation_task", +) -> list[AblationResult]: + """Run multiple ablation configurations and compare. + + This function: + 1. Sets up tracing + 2. Runs each configuration on the same task + 3. Collects metrics and evaluation scores + 4. Saves results and prints comparison + + Args: + configs: List of configurations to test + task_prompt: The task prompt to run + output_dir: Base directory for output (default: ~/.flow/ablations) + task_name: Name for the task (used in file paths) + + Returns: + List of ablation results + """ + # Setup output directory + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + if output_dir is None: + output_dir = Path.home() / ".flow" / "ablations" + output_dir = output_dir / timestamp + output_dir.mkdir(parents=True, exist_ok=True) + + # Create task + task = Task( + name=task_name, + prompt=task_prompt, + criteria=[ + EvalCriterion( + name="completion", + instruction="The task should be completed successfully", + ), + ], + ) + + # Save configs + with open(output_dir / "config.json", "w") as f: # noqa: ASYNC230 + json.dump({ + "task": task_prompt, + "timestamp": timestamp, + "configs": [asdict(c) for c in configs], + }, f, indent=2) + + print("=" * 80) + print(" FLOW ABLATION RUNNER") + print("=" * 80) + print(f" Task: {task_prompt[:60]}{'...' if len(task_prompt) > 60 else ''}") + print(f" Configs: {len(configs)}") + print(f" Output: {output_dir}") + print("=" * 80) + + # Setup tracing once + setup_tracing("flow-ablation") + + results = [] + for i, config in enumerate(configs, 1): + print(f"\n[{i}/{len(configs)}] Running: {config.name}") + print("-" * 40) + + # Each config gets its own workspace + workspace = output_dir / config.name / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + + result = await run_single_ablation( + config=config, + task=task, + workspace=workspace, + ) + + results.append(result) + save_ablation_result(result, output_dir) + + # Quick status + status = "OK" if result.run_result.success else "FAIL" + print(f" {status} | {result.run_result.duration_seconds:.1f}s | " + f"Tokens: {result.metrics.total_tokens} | Tools: {result.metrics.tool_call_count}") + + # Save comparison + comparison_data = [ + { + "name": r.config.name, + "success": r.run_result.success, + "duration_seconds": r.run_result.duration_seconds, + "metrics": metrics_to_dict(r.metrics), + "evaluation": { + "score": r.eval_score, + "passed": r.eval_passed, + }, + } + for r in results + ] + + with open(output_dir / "comparison.json", "w") as f: # noqa: ASYNC230 + json.dump({"task": task_prompt, "results": comparison_data}, f, indent=2) + + # Print comparison + print_comparison_table(comparison_data, "Ablation Comparison") + + print(f"\nResults saved to: {output_dir}") + + return results + + +# ============================================================================= +# Context Engineering Baseline Configurations +# ============================================================================= +# These configurations demonstrate the three main context engineering strategies: +# 1. Compaction - Reactive trimming via message stores +# 2. Agent-Managed Memory - Agent controls when to write/read/delete +# 3. Isolation - Sub-agent architecture prevents context pollution + + +# Baseline: No context engineering (for comparison) +CONTEXT_ENG_BASELINE = AblationConfig( + name="no_context_engineering", + enable_message_compaction=False, + enable_memory_tool=False, + enable_sub_agent=False, +) + +# Strategy 1: Compaction via Message Stores +# Uses HeadTailCompactingMessageStore to keep first N + last M messages +# Good for: Long-running sessions where middle context is less important +COMPACTION_ONLY = AblationConfig( + name="compaction_only", + enable_message_compaction=True, + enable_memory_tool=False, + enable_sub_agent=False, + compaction_head_size=10, # Keep task context + compaction_tail_size=40, # Keep recent work +) + +# Strategy 2: Agent-Managed Memory +# Agent decides when to save/retrieve information from persistent storage +# Good for: Cross-session memory, learning patterns, storing decisions +AGENT_MEMORY_ONLY = AblationConfig( + name="agent_memory_only", + enable_message_compaction=False, + enable_memory_tool=True, + enable_sub_agent=False, +) + +# Strategy 3: Isolation via Sub-Agent +# Delegate heavy research to sub-agent with isolated context +# Good for: Complex research tasks that would pollute main context +ISOLATION_ONLY = AblationConfig( + name="isolation_only", + enable_message_compaction=False, + enable_memory_tool=False, + enable_sub_agent=True, +) + +# Combined: All context engineering strategies +# Uses compaction + memory + isolation together +# Good for: Production systems with long-running, complex tasks +ALL_CONTEXT_ENGINEERING = AblationConfig( + name="all_context_engineering", + enable_message_compaction=True, + enable_memory_tool=True, + enable_sub_agent=True, + compaction_head_size=10, + compaction_tail_size=40, +) + +# Predefined list for running context engineering comparison +CONTEXT_ENGINEERING_CONFIGS = [ + CONTEXT_ENG_BASELINE, + COMPACTION_ONLY, + AGENT_MEMORY_ONLY, + ISOLATION_ONLY, + ALL_CONTEXT_ENGINEERING, +] + + +async def run_context_engineering_comparison( + task_prompt: str, + output_dir: Path | None = None, +) -> list[AblationResult]: + """Run a comparison of all context engineering strategies. + + This is a convenience function that runs all context engineering + baseline configurations against a single task for comparison. + + Args: + task_prompt: The task to run (should benefit from context management) + output_dir: Optional output directory for results + + Returns: + List of AblationResult for each strategy + + Example: + >>> results = await run_context_engineering_comparison( + ... "Research the authentication patterns in this codebase and " + ... "create a summary document with recommendations." + ... ) + """ + return await run_ablations( + configs=CONTEXT_ENGINEERING_CONFIGS, + task_prompt=task_prompt, + output_dir=output_dir, + task_name="context_engineering_comparison", + ) + + +# ============================================================================= +# Shared Utilities for Pareto Analysis +# ============================================================================= + + +def compute_pareto_frontier( + summaries: list[ConfigSummary], + score_key: str = "avg_score", + cost_key: str = "avg_tokens", +) -> list[str]: + """Compute Pareto frontier for multi-objective optimization. + + Identifies configurations that are not dominated by any other configuration. + A config is dominated if another config has better score AND lower tokens. + + Args: + summaries: List of ConfigSummary objects (or dicts with score/token keys) + score_key: Attribute name for the score metric (higher is better) + cost_key: Attribute name for the cost metric (lower is better) + + Returns: + List of names of Pareto-optimal configurations + """ + # Sort by cost (ascending) + def get_val(s: object, key: str) -> float: + if isinstance(s, dict): + return float(s.get(key, 0)) + return float(getattr(s, key, 0)) + + def get_name(s: object) -> str: + if isinstance(s, dict): + return str(s.get("name", "")) + return str(getattr(s, "name", "")) + + sorted_summaries = sorted(summaries, key=lambda s: get_val(s, cost_key)) + + pareto_names = [] + best_score = -1.0 + + for summary in sorted_summaries: + score = get_val(summary, score_key) + if score > best_score: + pareto_names.append(get_name(summary)) + best_score = score + + return pareto_names + + +def generate_recommendation( + summaries: list[ConfigSummary], + pareto_names: list[str], + min_score: float = 0.7, +) -> tuple[str | None, str]: + """Generate a recommendation based on Pareto analysis. + + Args: + summaries: List of ConfigSummary objects + pareto_names: Names of Pareto-optimal configs + min_score: Minimum acceptable score threshold + + Returns: + Tuple of (recommended_config_name, recommendation_text) + """ + def get_val(s: object, key: str) -> float: + if isinstance(s, dict): + return float(s.get(key, 0)) + return float(getattr(s, key, 0)) + + def get_name(s: object) -> str: + if isinstance(s, dict): + return str(s.get("name", "")) + return str(getattr(s, "name", "")) + + # Filter to acceptable configs + acceptable = [s for s in summaries if get_val(s, "avg_score") >= min_score] + if not acceptable: + return None, "No configuration met the minimum score threshold." + + # Prefer Pareto-optimal configs + pareto_acceptable = [s for s in acceptable if get_name(s) in pareto_names] + candidates = pareto_acceptable if pareto_acceptable else acceptable + + # Pick the one with lowest tokens among candidates + best = min(candidates, key=lambda s: get_val(s, "avg_tokens")) + name = get_name(best) + tokens = get_val(best, "avg_tokens") + score = get_val(best, "avg_score") + + return name, f"Recommended: {name} (avg {tokens:.0f} tokens, {score:.2f} score)" diff --git a/src/flow/experiments/config_export.py b/src/flow/experiments/config_export.py new file mode 100644 index 0000000000000000000000000000000000000000..c1709431ee3356d39afa843d23477fe5a3f5dbc6 --- /dev/null +++ b/src/flow/experiments/config_export.py @@ -0,0 +1,184 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Config export/import utilities for optimizer results. + +Exports winning configurations as YAML files that can be loaded +and used directly with `flow run --config `. +""" + +from __future__ import annotations + +from dataclasses import asdict +from pathlib import Path +from typing import Any + +import yaml + +from .ablation import AblationConfig + + +def export_config( + config: AblationConfig, + metrics: dict[str, Any], + path: Path, +) -> None: + """Export an AblationConfig as a reusable YAML file. + + The exported YAML includes: + - All config parameters (directly loadable) + - Optimization metadata prefixed with _ (ignored when loading) + + Args: + config: The AblationConfig to export + metrics: Optimization metrics (score, tokens, etc.) + path: Path to write the YAML file + + Example output: + name: compaction_head10_tail40 + enable_message_compaction: true + compaction_head_size: 10 + ... + _optimization: + timestamp: "2026-01-26T14:30:22" + avg_score: 0.89 + avg_tokens: 12400 + """ + data = asdict(config) + data["_optimization"] = metrics + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False)) + + +def load_config(path: Path) -> AblationConfig: + """Load an AblationConfig from a YAML file. + + Ignores any keys prefixed with _ (optimization metadata). + + Args: + path: Path to the YAML config file + + Returns: + AblationConfig instance + + Raises: + FileNotFoundError: If the config file doesn't exist + ValueError: If the config is invalid + """ + if not path.exists(): + raise FileNotFoundError(f"Config file not found: {path}") + + data = yaml.safe_load(path.read_text()) + + # Filter out metadata keys (prefixed with _) + config_data = {k: v for k, v in data.items() if not k.startswith("_")} + + try: + return AblationConfig(**config_data) + except TypeError as e: + raise ValueError(f"Invalid config file {path}: {e}") from e + + +def export_optimization_configs( + summaries: list[dict[str, Any]], + pareto_names: list[str], + output_dir: Path, + timestamp: str, +) -> dict[str, Path]: + """Export all notable configs from an optimization run. + + Exports: + - best_score.yaml: Highest quality config + - best_cost.yaml: Lowest token usage config + - best_efficiency.yaml: Best score/token ratio + - pareto/.yaml: All Pareto-optimal configs + + Args: + summaries: List of ConfigSummary dicts with metrics + pareto_names: Names of Pareto-optimal configs + output_dir: Directory to write configs + timestamp: Optimization timestamp for metadata + + Returns: + Dict mapping config type to file path + """ + configs_dir = output_dir / "configs" + configs_dir.mkdir(parents=True, exist_ok=True) + + exported: dict[str, Path] = {} + + if not summaries: + return exported + + # Find best by different criteria + best_score = max(summaries, key=lambda s: s.get("avg_score", 0)) + best_cost = min(summaries, key=lambda s: s.get("avg_tokens", float("inf"))) + best_efficiency = max( + summaries, + key=lambda s: s.get("avg_score", 0) / max(s.get("avg_tokens", 1), 1), + ) + + # Export best configs + for label, summary in [ + ("best_score", best_score), + ("best_cost", best_cost), + ("best_efficiency", best_efficiency), + ]: + config = _summary_to_config(summary) + metrics = _extract_metrics(summary, timestamp, label) + path = configs_dir / f"{label}.yaml" + export_config(config, metrics, path) + exported[label] = path + + # Export Pareto-optimal configs + pareto_dir = configs_dir / "pareto" + pareto_dir.mkdir(exist_ok=True) + + for summary in summaries: + name = summary.get("name", "unknown") + if name in pareto_names: + config = _summary_to_config(summary) + metrics = _extract_metrics(summary, timestamp, "pareto") + metrics["is_pareto_optimal"] = True + path = pareto_dir / f"{name}.yaml" + export_config(config, metrics, path) + exported[f"pareto/{name}"] = path + + return exported + + +def _summary_to_config(summary: dict[str, Any]) -> AblationConfig: + """Convert a summary dict back to an AblationConfig.""" + # Extract config fields from summary + config_fields = { + "name": summary.get("name", "unknown"), + "enable_message_compaction": summary.get("enable_message_compaction", True), + "enable_memory_tool": summary.get("enable_memory_tool", True), + "enable_sub_agent": summary.get("enable_sub_agent", False), + "compaction_head_size": summary.get("compaction_head_size", 10), + "compaction_tail_size": summary.get("compaction_tail_size", 40), + "bash_timeout": summary.get("bash_timeout", 120), + } + + # Also check nested config if present + if "config" in summary: + config_fields.update(summary["config"]) + + return AblationConfig(**config_fields) + + +def _extract_metrics( + summary: dict[str, Any], + timestamp: str, + selection_reason: str, +) -> dict[str, Any]: + """Extract optimization metrics from a summary.""" + return { + "timestamp": timestamp, + "selection_reason": selection_reason, + "avg_score": summary.get("avg_score", 0), + "avg_tokens": summary.get("avg_tokens", 0), + "avg_duration": summary.get("avg_duration", 0), + "pass_rate": summary.get("pass_rate", 0), + "pareto_rank": summary.get("pareto_rank"), + "is_pareto_optimal": summary.get("is_pareto_optimal", False), + } diff --git a/src/flow/experiments/evaluators/__init__.py b/src/flow/experiments/evaluators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..73c4ac2fc8cd63d55b22bdd51c4700337bf8e388 --- /dev/null +++ b/src/flow/experiments/evaluators/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Evaluators for the experiments framework.""" + +from .base import Evaluator +from .composite import CompositeEvaluator +from .heuristic import HeuristicEvaluator +from .llm import LLMEvaluator +from .trace import TraceEvaluator + +__all__ = [ + "CompositeEvaluator", + "Evaluator", + "HeuristicEvaluator", + "LLMEvaluator", + "TraceEvaluator", +] diff --git a/src/flow/experiments/evaluators/base.py b/src/flow/experiments/evaluators/base.py new file mode 100644 index 0000000000000000000000000000000000000000..eded29c00bf0b1e5e2507f8932e0fe177c60497b --- /dev/null +++ b/src/flow/experiments/evaluators/base.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Base evaluator protocol for the experiments framework.""" + +from typing import Protocol + +from ..types import EvalResult, RunResult + + +class Evaluator(Protocol): + """Protocol for evaluating agent outputs. + + Evaluators assess the results of agent runs and produce scores + and pass/fail determinations based on various criteria. + + Implementations: + - TraceEvaluator: Based on trace metrics (tokens, duration, tool calls) + - LLMEvaluator: Uses an LLM to judge output quality + - HeuristicEvaluator: Rule-based evaluation (files created, syntax, etc.) + - CompositeEvaluator: Combines multiple evaluators + """ + + async def evaluate(self, run_result: RunResult) -> EvalResult: + """Evaluate the result of an agent run. + + Args: + run_result: The result from running an agent on a task + + Returns: + EvalResult with scores and reasoning + """ + ... diff --git a/src/flow/experiments/evaluators/composite.py b/src/flow/experiments/evaluators/composite.py new file mode 100644 index 0000000000000000000000000000000000000000..df07e00472cd34826e32ab3da5b1b196f5da5e1d --- /dev/null +++ b/src/flow/experiments/evaluators/composite.py @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Composite evaluator that combines multiple evaluators.""" + +from typing import TYPE_CHECKING + +from ..types import EvalResult, RunResult + +if TYPE_CHECKING: + from .base import Evaluator + + +class CompositeEvaluator: + """Evaluator that combines multiple evaluators. + + Useful for combining different evaluation strategies: + - LLM evaluation with trace-based metrics + - Multiple heuristic checks + - Weighted combination of evaluators + + Example: + evaluator = CompositeEvaluator([ + TraceEvaluator(max_tokens=5000), + HeuristicEvaluator(), + ], weights=[0.3, 0.7]) + result = await evaluator.evaluate(run_result) + """ + + def __init__( + self, + evaluators: list["Evaluator"], + weights: list[float] | None = None, + ) -> None: + """Initialize the composite evaluator. + + Args: + evaluators: List of evaluators to combine + weights: Optional weights for each evaluator (default: equal weights) + + Raises: + ValueError: If number of weights doesn't match number of evaluators + """ + self.evaluators = evaluators + self.weights = weights or [1.0] * len(evaluators) + + if len(self.weights) != len(self.evaluators): + raise ValueError("Number of weights must match number of evaluators") + + async def evaluate(self, run_result: RunResult) -> EvalResult: + """Run all evaluators and combine results. + + The overall score is a weighted average of all evaluator scores. + The overall pass/fail is determined by whether ALL evaluators pass. + + Args: + run_result: The result from running an agent on a task + + Returns: + Combined EvalResult + """ + all_criteria_results = [] + total_weighted_score = 0.0 + total_weight = sum(self.weights) + all_passed = True + all_reasoning = [] + + for evaluator, weight in zip(self.evaluators, self.weights, strict=True): + result = await evaluator.evaluate(run_result) + all_criteria_results.extend(result.criteria_results) + total_weighted_score += result.score * weight + all_passed = all_passed and result.passed + if result.reasoning: + all_reasoning.append(result.reasoning) + + return EvalResult( + score=total_weighted_score / total_weight if total_weight > 0 else 0.0, + passed=all_passed, + criteria_results=all_criteria_results, + reasoning=" | ".join(all_reasoning), + ) diff --git a/src/flow/experiments/evaluators/heuristic.py b/src/flow/experiments/evaluators/heuristic.py new file mode 100644 index 0000000000000000000000000000000000000000..ffb491a856773a0e51b53749924307950fc9deb4 --- /dev/null +++ b/src/flow/experiments/evaluators/heuristic.py @@ -0,0 +1,193 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Heuristic evaluator using rule-based assessment.""" + +import logging +import subprocess + +from ..types import CriterionResult, EvalResult, RunResult + +logger = logging.getLogger(__name__) + + +class HeuristicEvaluator: + """Evaluator that uses heuristic rules to assess agent output. + + This evaluator checks: + 1. Were files created? + 2. Do Python files have valid syntax? + 3. Did the agent report completion? + 4. Does the output match expected patterns based on the task? + + Useful for quick, deterministic evaluation without LLM calls. + + Example: + evaluator = HeuristicEvaluator(passing_threshold=0.5) + result = await evaluator.evaluate(run_result) + print(f"Score: {result.score}, Passed: {result.passed}") + """ + + def __init__(self, passing_threshold: float = 0.5) -> None: + """Initialize the heuristic evaluator. + + Args: + passing_threshold: Minimum score to pass (0.0 to 1.0) + """ + self.passing_threshold = passing_threshold + + async def evaluate(self, run_result: RunResult) -> EvalResult: + """Evaluate the agent's output using heuristic rules. + + Args: + run_result: The result from running an agent on a task + + Returns: + EvalResult with heuristic-based scores + """ + criteria_results = [] + notes = [] + score = 0.0 + + # Check if files were created + if run_result.files_created: + criteria_results.append( + CriterionResult( + name="files_created", + score=1.0, + passed=True, + reasoning=f"Created {len(run_result.files_created)} file(s)", + ) + ) + score += 0.25 + notes.append(f"Created {len(run_result.files_created)} file(s)") + else: + criteria_results.append( + CriterionResult( + name="files_created", + score=0.0, + passed=False, + reasoning="No files created", + ) + ) + notes.append("No files created") + + # Check if agent reported task complete + output_lower = run_result.output.lower() + if "task_done" in output_lower or "complete" in output_lower or "finished" in output_lower: + criteria_results.append( + CriterionResult( + name="task_completed", + score=1.0, + passed=True, + reasoning="Agent reported completion", + ) + ) + score += 0.25 + notes.append("Agent reported completion") + else: + criteria_results.append( + CriterionResult( + name="task_completed", + score=0.0, + passed=False, + reasoning="Agent did not report completion", + ) + ) + + # Try to validate Python files (check syntax) + python_files = [f for f in run_result.files_created if f.endswith(".py")] + if python_files: + all_valid = True + syntax_notes = [] + for py_file in python_files[:5]: # Check up to 5 files + file_path = run_result.workspace / py_file + if file_path.exists(): + try: + result = subprocess.run( # noqa: ASYNC221, S603 + ["python3", "-m", "py_compile", str(file_path)], # noqa: S607 + capture_output=True, + timeout=5, + ) + if result.returncode != 0: + all_valid = False + syntax_notes.append(f"Syntax error in {py_file}") + except subprocess.TimeoutExpired: + syntax_notes.append(f"Timeout checking {py_file}") + except FileNotFoundError: + # python3 not available, skip syntax check + pass + except Exception as e: + all_valid = False + syntax_notes.append(f"Error checking {py_file}: {e}") + + if all_valid and not syntax_notes: + criteria_results.append( + CriterionResult( + name="code_syntax", + score=1.0, + passed=True, + reasoning="Python files have valid syntax", + ) + ) + score += 0.25 + notes.append("Python files have valid syntax") + elif syntax_notes: + criteria_results.append( + CriterionResult( + name="code_syntax", + score=0.0, + passed=False, + reasoning="; ".join(syntax_notes), + ) + ) + notes.extend(syntax_notes) + + # Check for expected patterns in output based on task + task_lower = run_result.task.prompt.lower() + output_correct = False + + if "hello" in task_lower and "hello" in output_lower: + output_correct = True + elif "api" in task_lower and ( + "fastapi" in output_lower or "endpoint" in output_lower or "flask" in output_lower + ): + output_correct = True + elif "http" in task_lower and ("server" in output_lower or "port" in output_lower): + output_correct = True + elif "test" in task_lower and ("pytest" in output_lower or "test" in output_lower): + output_correct = True + elif run_result.files_created: + # Generic: if files created, give partial credit + score += 0.125 + + if output_correct: + criteria_results.append( + CriterionResult( + name="output_relevance", + score=1.0, + passed=True, + reasoning="Output matches expected patterns for task", + ) + ) + score += 0.25 + + # Check for execution errors + if run_result.error: + criteria_results.append( + CriterionResult( + name="execution_success", + score=0.0, + passed=False, + reasoning=f"Execution failed: {run_result.error}", + ) + ) + score = max(0.0, score - 0.25) + + final_score = min(score, 1.0) + + return EvalResult( + score=final_score, + passed=final_score >= self.passing_threshold, + criteria_results=criteria_results, + reasoning="; ".join(notes) if notes else "Heuristic evaluation complete", + ) diff --git a/src/flow/experiments/evaluators/llm.py b/src/flow/experiments/evaluators/llm.py new file mode 100644 index 0000000000000000000000000000000000000000..c60e8d136e9298bd2b27a35d49426b0a9d1c969f --- /dev/null +++ b/src/flow/experiments/evaluators/llm.py @@ -0,0 +1,223 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""LLM-as-judge evaluator for quality assessment.""" + +import json +import logging +from typing import Any + +from ..metrics import extract_metrics +from ..types import CriterionResult, EvalResult, RunResult + +logger = logging.getLogger(__name__) + + +class LLMEvaluator: + """Evaluator that uses an LLM to assess agent output against criteria. + + This implements the LLM-as-a-judge pattern, where a language model + evaluates whether the agent's output meets specified criteria. + + Note: Requires a separate model client - not tied to FlowConfig. + This allows using a different model for evaluation than for agent execution. + + Example: + from openai import AsyncOpenAI + + client = AsyncOpenAI() + evaluator = LLMEvaluator( + model_client=client, + model_name="gpt-4o", + passing_threshold=0.7, + ) + result = await evaluator.evaluate(run_result) + """ + + def __init__( + self, + model_client: Any, + model_name: str = "gpt-4o", + passing_threshold: float = 0.7, + ) -> None: + """Initialize the LLM evaluator. + + Args: + model_client: An async client with chat.completions.create method + (e.g., AsyncOpenAI, AsyncAzureOpenAI) + model_name: Model name/deployment to use for evaluation + passing_threshold: Minimum score to pass (0.0 to 1.0) + """ + self.model_client = model_client + self.model_name = model_name + self.passing_threshold = passing_threshold + + def _get_evaluation_prompt(self, run_result: RunResult) -> str: + """Build the evaluation prompt for the LLM.""" + criteria_text = "\n".join( + f"- **{c.name}** (weight: {c.weight}): {c.instruction}" + for c in run_result.task.criteria + ) + + # Extract execution trace summary for research/multi-step tasks + trace_summary = self._get_trace_summary(run_result) + + return f"""You are an expert evaluator assessing an AI agent's output. + +## Task +The agent was given this task: +``` +{run_result.task.prompt} +``` + +## Agent Output +``` +{run_result.output[:8000]} +``` + +## Files Created +{json.dumps(run_result.files_created, indent=2) if run_result.files_created else "None"} + +## Execution Trace +{trace_summary} + +## Execution Status +{"Success" if run_result.success else f"Failed: {run_result.error}"} + +## Evaluation Criteria +{criteria_text} + +## Instructions +Evaluate the agent's output against each criterion. Consider both the final output AND the execution +trace (tools used, steps taken) when assessing correctness. + +For each criterion: +1. Assess how well the output meets the criterion (0.0 to 1.0) +2. Determine if it passes (score >= 0.7) +3. Provide brief reasoning + +Respond in this exact JSON format: +```json +{{ + "criteria_results": [ + {{ + "name": "criterion_name", + "score": 0.85, + "passed": true, + "reasoning": "Brief explanation" + }} + ], + "overall_reasoning": "Summary of the overall evaluation" +}} +``` +""" + + def _get_trace_summary(self, run_result: RunResult) -> str: + """Extract a summary of the execution trace for evaluation.""" + if not run_result.trace: + return "No trace data available" + + metrics = extract_metrics(run_result.trace) + + # Build tool usage summary + tool_summary = "" + if metrics.tool_calls_by_name: + tool_lines = [f" - {name}: {count}x" for name, count in metrics.tool_calls_by_name.items()] + tool_summary = "Tools used:\n" + "\n".join(tool_lines) + else: + tool_summary = "Tools used: None" + + return f"""Duration: {run_result.duration_seconds:.1f}s +LLM calls: {metrics.llm_call_count} +Total tool calls: {metrics.tool_call_count} +{tool_summary} +Tokens used: {metrics.total_tokens} (input: {metrics.input_tokens}, output: {metrics.output_tokens})""" + + async def evaluate(self, run_result: RunResult) -> EvalResult: + """Evaluate the agent's output using an LLM. + + Args: + run_result: The result from running an agent on a task + + Returns: + EvalResult with LLM-generated scores and reasoning + """ + if not run_result.task.criteria: + # No criteria to evaluate - return a default pass + return EvalResult( + score=1.0 if run_result.success else 0.0, + passed=run_result.success, + criteria_results=[], + reasoning=( + "No evaluation criteria specified" + + ("" if run_result.success else f"; Error: {run_result.error}") + ), + ) + + prompt = self._get_evaluation_prompt(run_result) + + try: + response = await self.model_client.chat.completions.create( + model=self.model_name, + messages=[ + { + "role": "system", + "content": "You are an expert evaluator. Respond only with valid JSON.", + }, + {"role": "user", "content": prompt}, + ], + temperature=0.1, # Low temperature for consistent evaluation + ) + + # Extract the response text + response_text = response.choices[0].message.content or "" + + # Parse JSON from response + json_start = response_text.find("{") + json_end = response_text.rfind("}") + 1 + if json_start >= 0 and json_end > json_start: + eval_data = json.loads(response_text[json_start:json_end]) + else: + raise ValueError("No JSON found in response") + + # Build criterion results + criteria_results = [] + total_weighted_score = 0.0 + total_weight = 0.0 + + for cr_data in eval_data.get("criteria_results", []): + cr = CriterionResult( + name=cr_data.get("name", "unknown"), + score=float(cr_data.get("score", 0.0)), + passed=bool(cr_data.get("passed", False)), + reasoning=cr_data.get("reasoning", ""), + ) + criteria_results.append(cr) + + # Find the weight for this criterion + weight = 1.0 + for task_criterion in run_result.task.criteria: + if task_criterion.name == cr.name: + weight = task_criterion.weight + break + + total_weighted_score += cr.score * weight + total_weight += weight + + # Calculate overall score + overall_score = total_weighted_score / total_weight if total_weight > 0 else 0.0 + + return EvalResult( + score=overall_score, + passed=overall_score >= self.passing_threshold, + criteria_results=criteria_results, + reasoning=eval_data.get("overall_reasoning", ""), + ) + + except Exception as e: + logger.error(f"LLM evaluation failed: {e}") + return EvalResult( + score=0.0, + passed=False, + criteria_results=[], + reasoning=f"Evaluation failed: {e}", + ) diff --git a/src/flow/experiments/evaluators/trace.py b/src/flow/experiments/evaluators/trace.py new file mode 100644 index 0000000000000000000000000000000000000000..100f5bb3098fba7759ae89259d65be04d3d88f07 --- /dev/null +++ b/src/flow/experiments/evaluators/trace.py @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Trace-based evaluator for objective metrics assessment.""" + +from ..metrics import extract_metrics +from ..types import CriterionResult, EvalResult, RunResult + + +class TraceEvaluator: + """Evaluator that assesses agent output based on trace metrics. + + This evaluator checks objective metrics from the execution trace, + such as token usage, tool calls, and timing. All limits are optional - + only specified limits are evaluated. + + Example: + evaluator = TraceEvaluator( + max_tokens=5000, + max_tool_calls=20, + max_duration_seconds=60.0, + ) + result = await evaluator.evaluate(run_result) + print(f"Passed: {result.passed}, Score: {result.score}") + """ + + def __init__( + self, + max_tokens: int | None = None, + max_tool_calls: int | None = None, + max_duration_seconds: float | None = None, + ) -> None: + """Initialize the trace evaluator. + + Args: + max_tokens: Maximum allowed total tokens (None = no limit) + max_tool_calls: Maximum allowed tool calls (None = no limit) + max_duration_seconds: Maximum allowed duration (None = no limit) + """ + self.max_tokens = max_tokens + self.max_tool_calls = max_tool_calls + self.max_duration_seconds = max_duration_seconds + + async def evaluate(self, run_result: RunResult) -> EvalResult: + """Evaluate the agent's output based on trace metrics. + + Args: + run_result: The result from running an agent on a task + + Returns: + EvalResult with metric-based scores + """ + metrics = extract_metrics(run_result.trace) + criteria_results = [] + all_passed = True + + # Check token limit + if self.max_tokens is not None: + passed = metrics.total_tokens <= self.max_tokens + all_passed = all_passed and passed + # Score decreases proportionally when over limit + if passed: + score = 1.0 + else: + overage = metrics.total_tokens - self.max_tokens + score = max(0.0, 1.0 - (overage / self.max_tokens)) + + criteria_results.append( + CriterionResult( + name="token_limit", + score=score, + passed=passed, + reasoning=f"Used {metrics.total_tokens} tokens (limit: {self.max_tokens})", + ) + ) + + # Check tool call limit + if self.max_tool_calls is not None: + passed = metrics.tool_call_count <= self.max_tool_calls + all_passed = all_passed and passed + if passed: + score = 1.0 + else: + overage = metrics.tool_call_count - self.max_tool_calls + score = max(0.0, 1.0 - (overage / self.max_tool_calls)) + + criteria_results.append( + CriterionResult( + name="tool_call_limit", + score=score, + passed=passed, + reasoning=f"Made {metrics.tool_call_count} tool calls (limit: {self.max_tool_calls})", + ) + ) + + # Check duration limit + if self.max_duration_seconds is not None: + passed = run_result.duration_seconds <= self.max_duration_seconds + all_passed = all_passed and passed + if passed: + score = 1.0 + else: + overage = run_result.duration_seconds - self.max_duration_seconds + score = max(0.0, 1.0 - (overage / self.max_duration_seconds)) + + criteria_results.append( + CriterionResult( + name="duration_limit", + score=score, + passed=passed, + reasoning=f"Took {run_result.duration_seconds:.2f}s (limit: {self.max_duration_seconds}s)", + ) + ) + + # Check for execution errors + if run_result.error: + all_passed = False + criteria_results.append( + CriterionResult( + name="execution_success", + score=0.0, + passed=False, + reasoning=f"Execution failed: {run_result.error}", + ) + ) + + # Check for trace errors + if metrics.error_count > 0: + criteria_results.append( + CriterionResult( + name="trace_errors", + score=max(0.0, 1.0 - (metrics.error_count * 0.2)), + passed=metrics.error_count == 0, + reasoning=f"Found {metrics.error_count} error(s) in trace", + ) + ) + + # Calculate overall score + if criteria_results: + overall_score = sum(cr.score for cr in criteria_results) / len(criteria_results) + else: + # No criteria specified - just check success + overall_score = 1.0 if run_result.success else 0.0 + + return EvalResult( + score=overall_score, + passed=all_passed and run_result.success, + criteria_results=criteria_results, + reasoning=f"Trace evaluation: {len(criteria_results)} criteria checked", + ) diff --git a/src/flow/experiments/metrics.py b/src/flow/experiments/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..59cf3ef37d49b141727d0ea46d9ab63feb1f8fc6 --- /dev/null +++ b/src/flow/experiments/metrics.py @@ -0,0 +1,267 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Metrics extraction utilities for the experiments framework.""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class LLMCallInfo: + """Information about a single LLM call.""" + + model: str = "unknown" + input_tokens: int = 0 + output_tokens: int = 0 + finish_reason: str = "" + duration_ms: float = 0.0 + + +@dataclass +class ToolCallInfo: + """Information about a single tool call.""" + + name: str = "unknown" + duration_ms: float = 0.0 + call_id: str = "" + + +@dataclass +class TraceMetrics: + """Objective metrics extracted from execution traces. + + These are factual measurements from the trace, not subjective assessments. + + Attributes: + total_tokens: Total tokens used (input + output) + input_tokens: Input/prompt tokens used + output_tokens: Output/completion tokens used + tool_call_count: Number of tool calls made + tool_calls_by_name: Count of calls per tool name + llm_call_count: Number of LLM API calls + total_duration_ms: Total execution time in milliseconds + llm_duration_ms: Time spent in LLM calls + tool_duration_ms: Time spent in tool calls + span_count: Total number of trace spans + error_count: Number of error spans + llm_calls: Detailed info for each LLM call + tool_calls: Detailed info for each tool call + """ + + total_tokens: int = 0 + input_tokens: int = 0 + output_tokens: int = 0 + tool_call_count: int = 0 + tool_calls_by_name: dict[str, int] = field(default_factory=dict) + llm_call_count: int = 0 + total_duration_ms: float = 0.0 + llm_duration_ms: float = 0.0 + tool_duration_ms: float = 0.0 + span_count: int = 0 + error_count: int = 0 + llm_calls: list[LLMCallInfo] = field(default_factory=list) + tool_calls: list[ToolCallInfo] = field(default_factory=list) + + +def extract_metrics(trace: list[dict[str, Any]]) -> TraceMetrics: + """Extract objective metrics from a trace. + + Parses OpenTelemetry semantic conventions for GenAI: + - gen_ai.operation.name == "chat" for LLM calls + - gen_ai.usage.input_tokens / output_tokens for token counts + - gen_ai.operation.name == "execute_tool" for tool calls + - gen_ai.tool.name for tool identification + + Args: + trace: List of trace span dictionaries + + Returns: + TraceMetrics with extracted values + """ + metrics = TraceMetrics() + metrics.span_count = len(trace) + + for span in trace: + data = span.get("data", {}) + attributes = data.get("attributes", {}) + operation_name = data.get("operation_name", "") + duration_ms = data.get("duration_ms", 0) or 0 + + # Check for errors + status = data.get("status", "") + if "ERROR" in str(status).upper(): + metrics.error_count += 1 + + # Check for LLM operations (gen_ai.operation.name = "chat") + if attributes.get("gen_ai.operation.name") == "chat": + input_tokens = attributes.get("gen_ai.usage.input_tokens", 0) or 0 + output_tokens = attributes.get("gen_ai.usage.output_tokens", 0) or 0 + + metrics.llm_call_count += 1 + metrics.input_tokens += int(input_tokens) + metrics.output_tokens += int(output_tokens) + metrics.llm_duration_ms += duration_ms + + metrics.llm_calls.append(LLMCallInfo( + model=attributes.get("gen_ai.request.model", "unknown"), + input_tokens=int(input_tokens), + output_tokens=int(output_tokens), + finish_reason=str(attributes.get("gen_ai.response.finish_reasons", "")), + duration_ms=duration_ms, + )) + + # Check for tool executions + elif attributes.get("gen_ai.operation.name") == "execute_tool": + tool_name = attributes.get("gen_ai.tool.name", operation_name) + + metrics.tool_call_count += 1 + metrics.tool_duration_ms += duration_ms + metrics.tool_calls_by_name[tool_name] = metrics.tool_calls_by_name.get(tool_name, 0) + 1 + + metrics.tool_calls.append(ToolCallInfo( + name=tool_name, + duration_ms=duration_ms, + call_id=attributes.get("gen_ai.tool.call.id", ""), + )) + + # Also check for generic tool patterns (fallback) + elif not attributes.get("gen_ai.operation.name"): + is_tool_call = ( + "tool" in operation_name.lower() + or attributes.get("tool.name") + or attributes.get("gen_ai.tool.name") + or "function_call" in operation_name.lower() + ) + + if is_tool_call: + tool_name = ( + attributes.get("tool.name") + or attributes.get("gen_ai.tool.name") + or _extract_tool_name_from_operation(operation_name) + or "unknown" + ) + metrics.tool_call_count += 1 + metrics.tool_duration_ms += duration_ms + metrics.tool_calls_by_name[tool_name] = metrics.tool_calls_by_name.get(tool_name, 0) + 1 + + metrics.tool_calls.append(ToolCallInfo( + name=tool_name, + duration_ms=duration_ms, + call_id="", + )) + + # Check for token counts in non-chat spans (fallback) + input_tokens = ( + attributes.get("gen_ai.usage.input_tokens") + or attributes.get("llm.token_count.prompt") + or attributes.get("input_tokens") + ) + output_tokens = ( + attributes.get("gen_ai.usage.output_tokens") + or attributes.get("llm.token_count.completion") + or attributes.get("output_tokens") + ) + + if input_tokens or output_tokens: + metrics.input_tokens += int(input_tokens or 0) + metrics.output_tokens += int(output_tokens or 0) + metrics.llm_call_count += 1 + metrics.llm_duration_ms += duration_ms + + # Track total duration from root span + if not data.get("parent_span_id"): + metrics.total_duration_ms = max(metrics.total_duration_ms, duration_ms) + + # Calculate total tokens + metrics.total_tokens = metrics.input_tokens + metrics.output_tokens + + return metrics + + +def _extract_tool_name_from_operation(operation_name: str) -> str | None: + """Try to extract a tool name from an operation name. + + Args: + operation_name: The span operation name + + Returns: + Extracted tool name or None + """ + # Common patterns: "tool:read_file", "execute_tool:write_file", "function_call:search" + for prefix in ["tool:", "execute_tool:", "function_call:", "call_"]: + if operation_name.lower().startswith(prefix): + return operation_name[len(prefix):] + + return None + + +def format_metrics_summary(metrics: TraceMetrics) -> str: + """Format metrics as a human-readable summary. + + Args: + metrics: TraceMetrics to format + + Returns: + Formatted string summary + """ + lines = [ + "=== Trace Metrics ===", + f"Tokens: {metrics.total_tokens} total ({metrics.input_tokens} input, {metrics.output_tokens} output)", + f"LLM Calls: {metrics.llm_call_count} ({metrics.llm_duration_ms:.1f}ms)", + f"Tool Calls: {metrics.tool_call_count} ({metrics.tool_duration_ms:.1f}ms)", + ] + + if metrics.tool_calls_by_name: + lines.append(" Tool breakdown:") + for name, count in sorted(metrics.tool_calls_by_name.items()): + lines.append(f" - {name}: {count}") + + lines.extend([ + f"Duration: {metrics.total_duration_ms:.2f}ms", + f"Spans: {metrics.span_count}", + f"Errors: {metrics.error_count}", + ]) + + return "\n".join(lines) + + +def metrics_to_dict(metrics: TraceMetrics) -> dict[str, Any]: + """Convert TraceMetrics to a JSON-serializable dictionary. + + Args: + metrics: TraceMetrics to convert + + Returns: + Dictionary representation + """ + return { + "total_tokens": metrics.total_tokens, + "input_tokens": metrics.input_tokens, + "output_tokens": metrics.output_tokens, + "tool_call_count": metrics.tool_call_count, + "tool_calls_by_name": metrics.tool_calls_by_name, + "llm_call_count": metrics.llm_call_count, + "total_duration_ms": metrics.total_duration_ms, + "llm_duration_ms": metrics.llm_duration_ms, + "tool_duration_ms": metrics.tool_duration_ms, + "span_count": metrics.span_count, + "error_count": metrics.error_count, + "llm_calls": [ + { + "model": c.model, + "input_tokens": c.input_tokens, + "output_tokens": c.output_tokens, + "finish_reason": c.finish_reason, + "duration_ms": c.duration_ms, + } + for c in metrics.llm_calls + ], + "tool_calls": [ + { + "name": c.name, + "duration_ms": c.duration_ms, + "call_id": c.call_id, + } + for c in metrics.tool_calls + ], + } diff --git a/src/flow/experiments/optimizer.py b/src/flow/experiments/optimizer.py new file mode 100644 index 0000000000000000000000000000000000000000..ff4fff393f5fbbe6bbc7ddddafade3d528a693f2 --- /dev/null +++ b/src/flow/experiments/optimizer.py @@ -0,0 +1,547 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Optimizer service for finding best agent configurations. + +Runs experiments in parallel, evaluates with LLM-as-Judge, +ranks via Pareto analysis, and exports reusable configs. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from collections.abc import Callable +from dataclasses import asdict, dataclass, field +from datetime import datetime +from itertools import product +from pathlib import Path +from typing import Any + +from openai import AsyncAzureOpenAI + +from .ablation import ( + AblationConfig, + compute_pareto_frontier, + create_harness_from_config, +) +from .config_export import export_optimization_configs +from .evaluators import LLMEvaluator +from .metrics import TraceMetrics, extract_metrics +from .runner import FlowExperimentRunner, setup_tracing +from .types import EvalCriterion, RunResult, Task + +logger = logging.getLogger(__name__) + + +@dataclass +class TaskResult: + """Result for a single config-task pair.""" + + config_name: str + task_name: str + run_result: RunResult + metrics: TraceMetrics + eval_score: float + eval_passed: bool + eval_reasoning: str + + +@dataclass +class ConfigSummary: + """Aggregated summary for a configuration across all tasks.""" + + name: str + config: AblationConfig + task_results: list[TaskResult] = field(default_factory=list) + + # Aggregated metrics + avg_score: float = 0.0 + avg_tokens: float = 0.0 + avg_duration: float = 0.0 + pass_rate: float = 0.0 + total_tokens: int = 0 + task_count: int = 0 + + # Pareto analysis + pareto_rank: int | None = None + is_pareto_optimal: bool = False + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for serialization.""" + return { + "name": self.name, + "config": asdict(self.config), + "avg_score": self.avg_score, + "avg_tokens": self.avg_tokens, + "avg_duration": self.avg_duration, + "pass_rate": self.pass_rate, + "total_tokens": self.total_tokens, + "task_count": self.task_count, + "pareto_rank": self.pareto_rank, + "is_pareto_optimal": self.is_pareto_optimal, + } + + +@dataclass +class OptimizationResult: + """Complete results from an optimization run.""" + + timestamp: str + output_dir: Path + summaries: list[ConfigSummary] + pareto_frontier: list[str] + exported_configs: dict[str, Path] + + # Rankings + rank_by_score: list[str] = field(default_factory=list) + rank_by_tokens: list[str] = field(default_factory=list) + rank_by_efficiency: list[str] = field(default_factory=list) + + # Stats + total_experiments: int = 0 + total_duration_seconds: float = 0.0 + + def get_best_config(self, criterion: str = "score") -> ConfigSummary | None: + """Get the best config by a criterion.""" + if criterion == "score": + names = self.rank_by_score + elif criterion == "tokens": + names = self.rank_by_tokens + elif criterion == "efficiency": + names = self.rank_by_efficiency + else: + return None + + if not names: + return None + + for summary in self.summaries: + if summary.name == names[0]: + return summary + return None + + +class FlowOptimizer: + """Optimizer for finding best agent configurations. + + Runs experiments in parallel, evaluates results, performs + Pareto analysis, and exports winning configs. + + Example: + optimizer = FlowOptimizer(parallel=4) + configs = [ + AblationConfig(name="baseline", enable_message_compaction=False), + AblationConfig(name="compaction", enable_message_compaction=True), + ] + tasks = [Task(name="test", prompt="Create hello world")] + result = await optimizer.optimize(configs, tasks) + print(f"Best: {result.rank_by_score[0]}") + """ + + def __init__( + self, + parallel: int = 4, + use_llm_evaluator: bool = True, + output_dir: Path | None = None, + ) -> None: + """Initialize the optimizer. + + Args: + parallel: Max concurrent experiments + use_llm_evaluator: Whether to use LLM for evaluation + output_dir: Base directory for results + """ + self.parallel = parallel + self.use_llm_evaluator = use_llm_evaluator + self.output_dir = output_dir or Path.home() / ".flow" / "optimizations" + + async def optimize( + self, + configs: list[AblationConfig], + tasks: list[Task], + progress_callback: Callable[[int, int, str, str], None] | None = None, + ) -> OptimizationResult: + """Run optimization across all configs and tasks. + + Args: + configs: Configurations to test + tasks: Tasks to run each config on + progress_callback: Optional callback(completed, total, config, task) + + Returns: + OptimizationResult with rankings and exported configs + """ + start_time = datetime.now() + timestamp = start_time.strftime("%Y%m%d_%H%M%S") + run_dir = self.output_dir / timestamp + run_dir.mkdir(parents=True, exist_ok=True) + + # Setup + setup_tracing("flow-optimizer") + self._save_config(configs, tasks, run_dir) + + print("=" * 70) + print(" FLOW OPTIMIZER") + print("=" * 70) + print(f" Configs: {len(configs)}") + print(f" Tasks: {len(tasks)}") + print(f" Total: {len(configs) * len(tasks)} experiments") + print(f" Parallel: {self.parallel}") + print(f" Output: {run_dir}") + print("=" * 70) + + # Create LLM evaluator if needed + evaluator = None + if self.use_llm_evaluator: + evaluator = self._create_evaluator() + + # Run all experiments in parallel + task_results = await self._run_parallel( + configs, tasks, run_dir, evaluator, progress_callback + ) + + # Aggregate by config + summaries = self._aggregate_results(task_results, configs) + + # Pareto analysis + pareto_names = self._compute_pareto(summaries) + + # Compute rankings + rank_by_score = sorted(summaries, key=lambda s: s.avg_score, reverse=True) + rank_by_tokens = sorted(summaries, key=lambda s: s.avg_tokens) + rank_by_efficiency = sorted( + summaries, + key=lambda s: s.avg_score / max(s.avg_tokens, 1), + reverse=True, + ) + + # Export configs + summary_dicts = [s.to_dict() for s in summaries] + exported = export_optimization_configs( + summary_dicts, pareto_names, run_dir, timestamp + ) + + end_time = datetime.now() + + result = OptimizationResult( + timestamp=timestamp, + output_dir=run_dir, + summaries=summaries, + pareto_frontier=pareto_names, + exported_configs=exported, + rank_by_score=[s.name for s in rank_by_score], + rank_by_tokens=[s.name for s in rank_by_tokens], + rank_by_efficiency=[s.name for s in rank_by_efficiency], + total_experiments=len(task_results), + total_duration_seconds=(end_time - start_time).total_seconds(), + ) + + # Save results + self._save_results(result, run_dir) + + # Print summary + self._print_summary(result) + + return result + + async def _run_parallel( + self, + configs: list[AblationConfig], + tasks: list[Task], + run_dir: Path, + evaluator: LLMEvaluator | None, + progress_callback: Callable[[int, int, str, str], None] | None, + ) -> list[TaskResult]: + """Run all config-task pairs in parallel with semaphore control.""" + semaphore = asyncio.Semaphore(self.parallel) + total = len(configs) * len(tasks) + completed = 0 + lock = asyncio.Lock() + + async def run_one(config: AblationConfig, task: Task) -> TaskResult: + nonlocal completed + async with semaphore: + workspace = run_dir / "workspaces" / config.name / task.name + workspace.mkdir(parents=True, exist_ok=True) + + result = await self._run_single(config, task, workspace, evaluator) + + async with lock: + completed += 1 + status = "βœ“" if result.eval_passed else "βœ—" + print( + f" [{completed}/{total}] {config.name}/{task.name}: " + f"{status} score={result.eval_score:.2f} " + f"tokens={result.metrics.total_tokens:,}" + ) + if progress_callback: + progress_callback(completed, total, config.name, task.name) + + return result + + # Create all tasks + coroutines = [run_one(config, task) for config in configs for task in tasks] + + # Run with gather + gather_results = await asyncio.gather(*coroutines, return_exceptions=True) + + # Filter out exceptions + valid_results: list[TaskResult] = [] + for r in gather_results: + if isinstance(r, BaseException): + logger.error(f"Experiment failed: {r}") + else: + valid_results.append(r) + + return valid_results + + async def _run_single( + self, + config: AblationConfig, + task: Task, + workspace: Path, + evaluator: LLMEvaluator | None, + ) -> TaskResult: + """Run a single config-task experiment.""" + harness = create_harness_from_config(config, workspace) + + try: + runner = FlowExperimentRunner(keep_workspace=True) + run_result = await runner.run(harness, task, workspace=workspace) + metrics = extract_metrics(run_result.trace) + + # Evaluate + if evaluator: + eval_result = await evaluator.evaluate(run_result) + eval_score = eval_result.score + eval_passed = eval_result.passed + eval_reasoning = eval_result.reasoning + else: + # Simple heuristic: passed if no error + eval_score = 1.0 if run_result.success else 0.0 + eval_passed = run_result.success + eval_reasoning = "Success" if run_result.success else run_result.error or "Failed" + + return TaskResult( + config_name=config.name, + task_name=task.name, + run_result=run_result, + metrics=metrics, + eval_score=eval_score, + eval_passed=eval_passed, + eval_reasoning=eval_reasoning, + ) + finally: + await harness.close() + + def _aggregate_results( + self, + task_results: list[TaskResult], + configs: list[AblationConfig], + ) -> list[ConfigSummary]: + """Aggregate task results into config summaries.""" + config_map = {c.name: c for c in configs} + results_by_config: dict[str, list[TaskResult]] = {c.name: [] for c in configs} + + for result in task_results: + if result.config_name in results_by_config: + results_by_config[result.config_name].append(result) + + summaries = [] + for name, results in results_by_config.items(): + if not results: + continue + + config = config_map[name] + summary = ConfigSummary( + name=name, + config=config, + task_results=results, + avg_score=sum(r.eval_score for r in results) / len(results), + avg_tokens=sum(r.metrics.total_tokens for r in results) / len(results), + avg_duration=sum(r.run_result.duration_seconds for r in results) / len(results), + pass_rate=sum(1 for r in results if r.eval_passed) / len(results), + total_tokens=sum(r.metrics.total_tokens for r in results), + task_count=len(results), + ) + summaries.append(summary) + + return summaries + + def _compute_pareto(self, summaries: list[ConfigSummary]) -> list[str]: + """Compute Pareto frontier (maximize score, minimize tokens).""" + # Use shared utility + pareto_names = compute_pareto_frontier(summaries) + + # Mark summaries with Pareto status + for summary in summaries: + if summary.name in pareto_names: + summary.is_pareto_optimal = True + summary.pareto_rank = 0 + else: + summary.is_pareto_optimal = False + summary.pareto_rank = 1 # Simplified: all non-Pareto get rank 1 + + return pareto_names + + def _create_evaluator(self) -> LLMEvaluator | None: + """Create LLM evaluator if credentials available.""" + api_key = os.environ.get("AZURE_OPENAI_API_KEY") + endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") + deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o") + + if not api_key or not endpoint: + logger.warning("No Azure OpenAI credentials, using heuristic evaluation") + return None + + client = AsyncAzureOpenAI( + api_key=api_key, + api_version="2024-02-15-preview", + azure_endpoint=endpoint, + ) + + return LLMEvaluator( + model_client=client, + model_name=deployment, + passing_threshold=0.7, + ) + + def _save_config( + self, + configs: list[AblationConfig], + tasks: list[Task], + run_dir: Path, + ) -> None: + """Save optimization config.""" + with open(run_dir / "optimization_config.json", "w") as f: + json.dump( + { + "configs": [asdict(c) for c in configs], + "tasks": [{"name": t.name, "prompt": t.prompt} for t in tasks], + "parallel": self.parallel, + "use_llm_evaluator": self.use_llm_evaluator, + }, + f, + indent=2, + ) + + def _save_results(self, result: OptimizationResult, run_dir: Path) -> None: + """Save optimization results.""" + summary_data = { + "timestamp": result.timestamp, + "total_experiments": result.total_experiments, + "total_duration_seconds": result.total_duration_seconds, + "pareto_frontier": result.pareto_frontier, + "rank_by_score": result.rank_by_score, + "rank_by_tokens": result.rank_by_tokens, + "rank_by_efficiency": result.rank_by_efficiency, + "exported_configs": {k: str(v) for k, v in result.exported_configs.items()}, + "summaries": [s.to_dict() for s in result.summaries], + } + + with open(run_dir / "summary.json", "w") as f: + json.dump(summary_data, f, indent=2) + + def _print_summary(self, result: OptimizationResult) -> None: + """Print optimization summary.""" + print("\n" + "=" * 70) + print(" OPTIMIZATION RESULTS") + print("=" * 70) + + # Rankings table + print(f"\n{'Config':<30} | {'Score':>8} | {'Tokens':>10} | {'Pareto':>8}") + print("-" * 65) + + for summary in sorted(result.summaries, key=lambda s: s.avg_score, reverse=True): + pareto = "β˜…" if summary.is_pareto_optimal else "" + print( + f"{summary.name:<30} | {summary.avg_score:>8.2f} | " + f"{summary.avg_tokens:>10,.0f} | {pareto:>8}" + ) + + print("\n" + "-" * 70) + print(f"Pareto frontier: {result.pareto_frontier}") + print(f"Best by score: {result.rank_by_score[0] if result.rank_by_score else 'N/A'}") + print(f"Best by efficiency: {result.rank_by_efficiency[0] if result.rank_by_efficiency else 'N/A'}") + print("\nExported configs:") + for name, path in result.exported_configs.items(): + print(f" {name}: {path}") + print(f"\nResults saved to: {result.output_dir}") + + +def generate_grid_configs( + base_name: str, + variations: dict[str, list[Any]], +) -> list[AblationConfig]: + """Generate configs from a variation grid. + + Args: + base_name: Base name for generated configs + variations: Dict of param_name -> list of values + + Returns: + List of AblationConfig for each combination + + Example: + configs = generate_grid_configs("grid", { + "enable_message_compaction": [True, False], + "compaction_head_size": [5, 10, 20], + }) + """ + if not variations: + return [AblationConfig(name=base_name)] + + param_names = list(variations.keys()) + param_values = list(variations.values()) + + configs = [] + for values in product(*param_values): + kwargs = dict(zip(param_names, values, strict=True)) + name = f"{base_name}_" + "_".join(f"{k}={v}" for k, v in kwargs.items()) + configs.append(AblationConfig(name=name, **kwargs)) + + return configs + + +def load_tasks_from_jsonl(path: Path) -> list[Task]: + """Load tasks from a JSONL file. + + Each line should be a JSON object with: + - name: Task name + - prompt: Task prompt + - criteria: Optional list of evaluation criteria + - category: Optional category string + + Args: + path: Path to JSONL file + + Returns: + List of Task objects + """ + tasks = [] + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + + data = json.loads(line) + criteria = [] + for c in data.get("criteria", []): + if isinstance(c, dict): + criteria.append(EvalCriterion(**c)) + else: + criteria.append(EvalCriterion(name="default", instruction=str(c))) + + tasks.append( + Task( + name=data["name"], + prompt=data["prompt"], + criteria=criteria, + metadata={"category": data.get("category", "default")}, + ) + ) + + return tasks diff --git a/src/flow/experiments/reporters/__init__.py b/src/flow/experiments/reporters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5cab09ed0203afa96cc62c239b97b2a4571ab95d --- /dev/null +++ b/src/flow/experiments/reporters/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Reporters for experiment results.""" + +from .console_reporter import print_comparison_table, print_eval_result, print_metrics_summary +from .json_reporter import load_run_result_summary, save_comparison, save_run_result + +__all__ = [ # noqa: RUF022 # Intentionally grouped by category + # JSON reporter + "save_run_result", + "load_run_result_summary", + "save_comparison", + # Console reporter + "print_metrics_summary", + "print_comparison_table", + "print_eval_result", +] diff --git a/src/flow/experiments/reporters/console_reporter.py b/src/flow/experiments/reporters/console_reporter.py new file mode 100644 index 0000000000000000000000000000000000000000..2402eba708fd97c434424cebb2dbcc0d8076b716 --- /dev/null +++ b/src/flow/experiments/reporters/console_reporter.py @@ -0,0 +1,135 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Console reporter for experiment results with rich formatting.""" + +from typing import Any + +from ..metrics import TraceMetrics + + +def print_metrics_summary(metrics: TraceMetrics, title: str = "Trace Metrics") -> None: + """Print a formatted metrics summary to console. + + Args: + metrics: TraceMetrics to display + title: Title for the summary section + """ + print(f"\n{'=' * 60}") + print(f" {title}") + print("=" * 60) + print(f" Tokens: {metrics.total_tokens:,} total ({metrics.input_tokens:,} in, {metrics.output_tokens:,} out)") + print(f" LLM Calls: {metrics.llm_call_count} ({metrics.llm_duration_ms:.1f}ms)") + print(f" Tool Calls: {metrics.tool_call_count} ({metrics.tool_duration_ms:.1f}ms)") + + if metrics.tool_calls_by_name: + print(" Tool breakdown:") + for name, count in sorted(metrics.tool_calls_by_name.items()): + print(f" - {name}: {count}") + + print(f" Duration: {metrics.total_duration_ms:.2f}ms") + print(f" Spans: {metrics.span_count}") + if metrics.error_count > 0: + print(f" Errors: {metrics.error_count}") + print("=" * 60) + + +def print_comparison_table( + results: list[dict[str, Any]], + title: str = "Comparison", +) -> None: + """Print a side-by-side comparison table of multiple results. + + Args: + results: List of result dictionaries with 'name' and 'metrics' keys + title: Title for the comparison + """ + if not results: + print("No results to compare") + return + + names = [r.get("name", "unknown") for r in results] + col_width = max(15, max(len(n) for n in names) + 2) + + print(f"\n{'=' * 80}") + print(f" {title}") + print("=" * 80) + + # Header + print(f"\n{'Metric':<30} | " + " | ".join(f"{n:>{col_width}}" for n in names)) + print("-" * (32 + (col_width + 3) * len(names))) + + def row(label: str, values: list[Any]) -> None: + formatted = [] + for v in values: + if isinstance(v, float): + formatted.append(f"{v:>{col_width}.1f}") + elif isinstance(v, bool): + formatted.append(f"{v!s:>{col_width}}") + else: + formatted.append(f"{v:>{col_width}}") + print(f"{label:<30} | " + " | ".join(formatted)) + + # Extract metrics for each result + metrics_list = [r.get("metrics", {}) for r in results] + + row("Duration (s)", [r.get("duration_seconds", 0) for r in results]) + row("Success", [r.get("success", False) for r in results]) + + if any(r.get("evaluation") for r in results): + row("Eval Score", [r.get("evaluation", {}).get("score", 0) for r in results]) + + row("Total Tokens", [m.get("total_tokens", 0) for m in metrics_list]) + row("Input Tokens", [m.get("input_tokens", 0) for m in metrics_list]) + row("Output Tokens", [m.get("output_tokens", 0) for m in metrics_list]) + row("LLM Calls", [m.get("llm_call_count", 0) for m in metrics_list]) + row("Tool Calls", [m.get("tool_call_count", 0) for m in metrics_list]) + row("LLM Time (ms)", [m.get("llm_duration_ms", 0) for m in metrics_list]) + row("Tool Time (ms)", [m.get("tool_duration_ms", 0) for m in metrics_list]) + + # Tool breakdown + all_tools: set[str] = set() + for m in metrics_list: + all_tools.update(m.get("tool_calls_by_name", {}).keys()) + + if all_tools: + print("\n" + "-" * 80) + print("Tool Usage Breakdown:") + for tool in sorted(all_tools): + values = [m.get("tool_calls_by_name", {}).get(tool, 0) for m in metrics_list] + row(f" {tool}", values) + + print("=" * 80) + + +def print_eval_result( + score: float, + passed: bool, + reasoning: str, + criteria_results: list[dict[str, Any]] | None = None, +) -> None: + """Print evaluation results in a formatted way. + + Args: + score: Overall score (0.0 to 1.0) + passed: Whether evaluation passed + reasoning: Overall reasoning + criteria_results: Optional list of individual criterion results + """ + status = "PASS" if passed else "FAIL" + + print(f"\n{'=' * 60}") + print(f" Evaluation Result: {status}") + print("=" * 60) + print(f" Score: {score:.2f}") + print(f" Passed: {passed}") + print(f" Reason: {reasoning}") + + if criteria_results: + print("\n Criteria:") + for cr in criteria_results: + cr_status = "PASS" if cr.get("passed") else "FAIL" + print(f" - {cr.get('name', 'unknown')}: {cr_status} ({cr.get('score', 0):.2f})") + if cr.get("reasoning"): + print(f" {cr['reasoning']}") + + print("=" * 60) diff --git a/src/flow/experiments/reporters/json_reporter.py b/src/flow/experiments/reporters/json_reporter.py new file mode 100644 index 0000000000000000000000000000000000000000..26b1442095f7727a7e28694d36d598d31790a5aa --- /dev/null +++ b/src/flow/experiments/reporters/json_reporter.py @@ -0,0 +1,133 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""JSON reporter for experiment results.""" + +import json +from dataclasses import asdict +from pathlib import Path +from typing import Any + +from ..metrics import TraceMetrics, metrics_to_dict +from ..types import EvalResult, RunResult + + +def save_run_result( + result: RunResult, + output_dir: Path, + eval_result: EvalResult | None = None, + metrics: TraceMetrics | None = None, +) -> None: + """Save a run result to JSON files. + + Creates the following files in output_dir: + - traces.json: Raw OpenTelemetry spans + - metrics.json: Extracted metrics (if provided) + - output.txt: Agent text output + - result.json: Full result summary + + Args: + result: The RunResult to save + output_dir: Directory to save files + eval_result: Optional evaluation result + metrics: Optional extracted metrics + """ + output_dir.mkdir(parents=True, exist_ok=True) + + # Save raw traces + with open(output_dir / "traces.json", "w") as f: + json.dump(result.trace, f, indent=2, default=str) + + # Save extracted metrics + if metrics: + with open(output_dir / "metrics.json", "w") as f: + json.dump(metrics_to_dict(metrics), f, indent=2) + + # Save agent output + with open(output_dir / "output.txt", "w") as f: + f.write(f"Task: {result.task.prompt}\n") + f.write(f"Duration: {result.duration_seconds:.1f}s\n") + f.write(f"Success: {result.success}\n") + if eval_result: + f.write(f"Eval Score: {eval_result.score:.2f}\n") + if result.error: + f.write(f"Error: {result.error}\n") + f.write("\n" + "=" * 60 + "\n\n") + f.write(result.output) + + # Save full result + result_dict: dict[str, Any] = { + "task": { + "name": result.task.name, + "prompt": result.task.prompt, + "criteria": [asdict(c) for c in result.task.criteria], + "metadata": result.task.metadata, + }, + "success": result.success, + "error": result.error, + "duration_seconds": result.duration_seconds, + "files_created": result.files_created, + "trace_count": len(result.trace), + "output_length": len(result.output), + } + + if metrics: + result_dict["metrics"] = metrics_to_dict(metrics) + + if eval_result: + result_dict["evaluation"] = { + "score": eval_result.score, + "passed": eval_result.passed, + "reasoning": eval_result.reasoning, + "criteria_results": [ + { + "name": cr.name, + "score": cr.score, + "passed": cr.passed, + "reasoning": cr.reasoning, + } + for cr in eval_result.criteria_results + ], + } + + with open(output_dir / "result.json", "w") as f: + json.dump(result_dict, f, indent=2) + + +def load_run_result_summary(result_path: Path) -> dict[str, Any]: + """Load a run result summary from a result.json file. + + Args: + result_path: Path to result.json file + + Returns: + Dictionary with result summary + """ + with open(result_path) as f: + return json.load(f) + + +def save_comparison( + results: list[tuple[str, dict[str, Any]]], + output_path: Path, +) -> None: + """Save a comparison of multiple results. + + Args: + results: List of (name, result_dict) tuples + output_path: Path to save comparison JSON + """ + comparison = { + "results": [ + { + "name": name, + "success": result.get("success"), + "duration_seconds": result.get("duration_seconds"), + "metrics": result.get("metrics"), + "evaluation": result.get("evaluation"), + } + for name, result in results + ], + } + + with open(output_path, "w") as f: + json.dump(comparison, f, indent=2) diff --git a/src/flow/experiments/runner.py b/src/flow/experiments/runner.py new file mode 100644 index 0000000000000000000000000000000000000000..ba94727fa3e68550b02cf499ce07f656128f81db --- /dev/null +++ b/src/flow/experiments/runner.py @@ -0,0 +1,243 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Experiment runner for executing agents on tasks with trace capture.""" + +from __future__ import annotations + +import logging +import os +import tempfile +import time +from pathlib import Path +from typing import TYPE_CHECKING + +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.semconv._incubating.attributes.service_attributes import SERVICE_NAME + +from .trace_collector import FlowTraceCollector +from .types import RunResult, Task + +if TYPE_CHECKING: + from flow.harness.maf import MAFHarness + +logger = logging.getLogger(__name__) + + +def setup_tracing(service_name: str = "flow-experiments") -> TracerProvider: + """Setup OpenTelemetry tracing with in-memory collection. + + This creates a new TracerProvider configured for experiment tracing. + Call this once at the start of your experiment session. + + Args: + service_name: Name for the tracing service + + Returns: + The configured TracerProvider + """ + resource = Resource.create({SERVICE_NAME: service_name}) + provider = TracerProvider(resource=resource) + trace.set_tracer_provider(provider) + + # Enable agent framework instrumentation if available + try: + from agent_framework.observability import enable_instrumentation + enable_instrumentation() + logger.debug("Agent Framework instrumentation enabled") + except ImportError: + logger.debug("Agent Framework not available, skipping instrumentation") + except Exception as e: + logger.debug(f"Could not enable Agent Framework instrumentation: {e}") + + return provider + + +class FlowExperimentRunner: + """Runner for executing experiments with Flow agents. + + The runner handles: + - Setting up temporary workspaces + - Collecting execution traces via OpenTelemetry + - Measuring execution time + - Capturing files created + - Supporting streaming execution + + Example: + from flow.harness.maf import MAFHarness + from flow.experiments import FlowExperimentRunner, Task + + harness = MAFHarness() + runner = FlowExperimentRunner(keep_workspace=True) + + task = Task(name="hello", prompt="Create a hello world script") + result = await runner.run(harness, task) + + print(f"Duration: {result.duration_seconds}s") + print(f"Files: {result.files_created}") + """ + + def __init__( + self, + workspace_base: Path | None = None, + keep_workspace: bool = False, + ) -> None: + """Initialize the experiment runner. + + Args: + workspace_base: Base directory for workspaces (default: system temp) + keep_workspace: Whether to keep workspace after run (default: False) + """ + self.workspace_base = workspace_base or Path(tempfile.gettempdir()) + self.keep_workspace = keep_workspace + + async def run( + self, + harness: MAFHarness, + task: Task, + workspace: Path | None = None, + ) -> RunResult: + """Run a harness on a task and collect results. + + This method: + 1. Creates or uses a workspace directory + 2. Sets up trace collection + 3. Executes the harness with streaming + 4. Collects output and files created + 5. Returns a RunResult with all data + + Args: + harness: The MAFHarness to run + task: The task to execute + workspace: Optional workspace directory (creates temp if None) + + Returns: + RunResult with trace, output, and metrics + """ + # Create or use workspace directory + if workspace is None: + workspace = Path(tempfile.mkdtemp( + prefix=f"flow_experiment_{task.name}_", + dir=self.workspace_base, + )) + workspace_created = True + else: + workspace.mkdir(parents=True, exist_ok=True) + workspace_created = False + + logger.info(f"Running task '{task.name}' in workspace: {workspace}") + + # Track files before execution + files_before = set(self._list_files(workspace)) + + # Set up trace collection + collector = FlowTraceCollector() + processor: SimpleSpanProcessor | None = None + + try: + provider = trace.get_tracer_provider() + if isinstance(provider, TracerProvider): + processor = SimpleSpanProcessor(collector) + provider.add_span_processor(processor) + logger.debug("Trace collection enabled") + except Exception as e: + logger.debug(f"Could not set up trace collection: {e}") + + # Execute the harness + start_time = time.time() + output_chunks: list[str] = [] + error: str | None = None + + try: + # Change to workspace directory for execution + original_cwd = os.getcwd() + os.chdir(workspace) + + try: + # Use streaming execution to capture all output + async for event in harness.run_stream(task.prompt): + # Collect text output + if hasattr(event, "content") and event.content: + if hasattr(event, "type"): + from ..harness.base import EventType + if event.type in (EventType.TEXT_DELTA, EventType.TEXT_DONE): + output_chunks.append(event.content) + elif event.type == EventType.TOOL_RESULT: + # Optionally capture tool results + pass + finally: + os.chdir(original_cwd) + + except Exception as e: + error = str(e) + logger.error(f"Task execution failed: {e}") + + end_time = time.time() + duration_seconds = end_time - start_time + + # Force flush and get traces + if processor: + try: + processor.force_flush() + except Exception as e: + logger.debug(f"Error flushing processor: {e}") + + # Get collected traces + trace_data = collector.get_traces() + + # Clean up trace processor + if processor: + try: + processor.shutdown() + except Exception as e: + logger.debug(f"Error shutting down processor: {e}") + + # Find files created + files_after = set(self._list_files(workspace)) + files_created = sorted(files_after - files_before) + + # Clean up workspace if not keeping and we created it + if not self.keep_workspace and workspace_created and not error: + try: + import shutil + shutil.rmtree(workspace) + logger.debug(f"Cleaned up workspace: {workspace}") + except Exception as e: + logger.warning(f"Failed to clean up workspace: {e}") + + output = "".join(output_chunks) + + return RunResult( + task=task, + trace=trace_data, + output=output, + files_created=files_created, + duration_seconds=duration_seconds, + workspace=workspace, + error=error, + ) + + def _list_files(self, directory: Path) -> list[str]: + """List all files in a directory recursively. + + Args: + directory: Directory to scan + + Returns: + List of relative file paths + """ + files = [] + try: + for root, _, filenames in os.walk(directory): + for filename in filenames: + # Skip hidden files and common temp files + if filename.startswith("."): + continue + full_path = Path(root) / filename + rel_path = full_path.relative_to(directory) + files.append(str(rel_path)) + except Exception as e: + logger.debug(f"Error listing files: {e}") + return files diff --git a/src/flow/experiments/trace_collector.py b/src/flow/experiments/trace_collector.py new file mode 100644 index 0000000000000000000000000000000000000000..7d7bc662cbb3244bb227d56ad903546500f4da26 --- /dev/null +++ b/src/flow/experiments/trace_collector.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""OpenTelemetry trace collector for experiment analysis.""" + +import logging +from datetime import datetime +from typing import Any + +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +logger = logging.getLogger(__name__) + + +class FlowTraceCollector(SpanExporter): + """Collects OpenTelemetry spans for experiment analysis. + + This exporter captures spans during agent execution and converts them + to a dictionary format suitable for metrics extraction and analysis. + + Example: + collector = FlowTraceCollector() + # Attach to TracerProvider via SimpleSpanProcessor + # Run agent execution + traces = collector.get_traces() + """ + + def __init__(self) -> None: + """Initialize the trace collector.""" + self.spans: list[dict[str, Any]] = [] + + def export(self, spans: Any) -> SpanExportResult: + """Collect spans from OpenTelemetry. + + Args: + spans: Sequence of OpenTelemetry ReadableSpan objects + + Returns: + SpanExportResult indicating success + """ + for span in spans: + try: + # Convert nanoseconds to seconds for timestamps + start_time = span.start_time / 1_000_000_000 + end_time = span.end_time / 1_000_000_000 if span.end_time else None + duration_ms = ((end_time - start_time) * 1000) if end_time else None + + self.spans.append({ + "type": "trace_span", + "timestamp": datetime.fromtimestamp(start_time).isoformat(), + "data": { + "operation_name": span.name, + "span_id": format(span.context.span_id, "016x"), + "trace_id": format(span.context.trace_id, "032x"), + "parent_span_id": ( + format(span.parent.span_id, "016x") if span.parent else None + ), + "duration_ms": duration_ms, + "attributes": dict(span.attributes) if span.attributes else {}, + "status": str(span.status.status_code.name) if hasattr(span, "status") else "OK", + "events": [ + { + "name": event.name, + "timestamp": datetime.fromtimestamp( + event.timestamp / 1_000_000_000 + ).isoformat(), + "attributes": dict(event.attributes) if event.attributes else {}, + } + for event in (span.events or []) + ], + }, + }) + except Exception as e: + logger.debug(f"Failed to collect span: {e}") + + return SpanExportResult.SUCCESS + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush spans (no-op for simple collection). + + Args: + timeout_millis: Timeout in milliseconds (unused) + + Returns: + True always + """ + return True + + def shutdown(self) -> None: + """Shutdown the exporter (no-op).""" + pass + + def get_traces(self) -> list[dict[str, Any]]: + """Get and clear collected traces. + + Returns: + List of collected trace spans, clearing the internal list + """ + traces = self.spans.copy() + self.spans.clear() + return traces + + def clear(self) -> None: + """Clear collected traces without returning them.""" + self.spans.clear() diff --git a/src/flow/experiments/types.py b/src/flow/experiments/types.py new file mode 100644 index 0000000000000000000000000000000000000000..b864a3d14ca20798e14a82861e58b8e364dad3f8 --- /dev/null +++ b/src/flow/experiments/types.py @@ -0,0 +1,266 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Type definitions for the experiments framework.""" + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +@dataclass +class EvalCriterion: + """A criterion for evaluating agent output. + + Attributes: + name: Short identifier for the criterion (e.g., "correctness", "completeness") + instruction: Detailed instruction for how to evaluate this criterion + weight: Relative weight for scoring (default 1.0) + """ + + name: str + instruction: str + weight: float = 1.0 + + +@dataclass +class Task: + """A task for the agent to perform. + + Attributes: + name: Short identifier for the task + prompt: The prompt/instruction given to the agent + criteria: List of evaluation criteria for assessing the output + metadata: Additional task metadata (e.g., expected output, difficulty) + """ + + name: str + prompt: str + criteria: list[EvalCriterion] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class RunResult: + """Result of running an agent on a task. + + Attributes: + task: The task that was executed + trace: OpenTelemetry trace spans collected during execution + output: The agent's final output/response + files_created: List of files created during execution + duration_seconds: Total execution time + workspace: Path to the workspace directory used + error: Error message if execution failed, None if successful + """ + + task: Task + trace: list[dict[str, Any]] + output: str + files_created: list[str] + duration_seconds: float + workspace: Path + error: str | None = None + + @property + def success(self) -> bool: + """Whether the run completed without errors.""" + return self.error is None + + +@dataclass +class CriterionResult: + """Result of evaluating a single criterion. + + Attributes: + name: Name of the criterion evaluated + score: Numeric score (0.0 to 1.0) + passed: Whether the criterion was met + reasoning: Explanation of the evaluation + """ + + name: str + score: float + passed: bool + reasoning: str + + +@dataclass +class EvalResult: + """Result of evaluating an agent's output. + + Attributes: + score: Overall weighted score (0.0 to 1.0) + passed: Whether the evaluation passed overall + criteria_results: Results for each individual criterion + reasoning: Overall evaluation reasoning/summary + """ + + score: float + passed: bool + criteria_results: list[CriterionResult] + reasoning: str + + +# ============================================================================= +# Built-in Task Suites for Optimization +# ============================================================================= + +TASK_SUITES: dict[str, list[Task]] = { + "quick": [ + Task( + name="fizzbuzz", + prompt="Create a Python file fizzbuzz.py that prints FizzBuzz from 1-100. Then run it.", + criteria=[ + EvalCriterion(name="file_created", instruction="fizzbuzz.py file was created"), + EvalCriterion(name="correct_output", instruction="Output shows correct FizzBuzz pattern"), + ], + metadata={"category": "short", "expected_duration": 60}, + ), + Task( + name="hello_api", + prompt="Create a FastAPI app in api.py with a /hello endpoint that returns {'message': 'hello'}.", + criteria=[ + EvalCriterion(name="file_created", instruction="api.py file was created"), + EvalCriterion(name="has_endpoint", instruction="Contains a /hello GET endpoint"), + ], + metadata={"category": "short", "expected_duration": 90}, + ), + Task( + name="file_counter", + prompt="Create a Python script count_files.py that counts .py files in current directory and prints the count.", + criteria=[ + EvalCriterion(name="file_created", instruction="count_files.py was created"), + EvalCriterion(name="runs_correctly", instruction="Script runs and outputs a number"), + ], + metadata={"category": "short", "expected_duration": 60}, + ), + ], + "core": [ + Task( + name="fizzbuzz", + prompt="Create a Python file fizzbuzz.py that prints FizzBuzz from 1-100. Then run it.", + criteria=[ + EvalCriterion(name="file_created", instruction="fizzbuzz.py file was created"), + EvalCriterion(name="correct_output", instruction="Output shows correct FizzBuzz pattern"), + ], + metadata={"category": "short"}, + ), + Task( + name="rest_api", + prompt="Create a FastAPI app with CRUD endpoints for a TODO list (in-memory storage). Include GET /todos, POST /todos, DELETE /todos/{id}.", + criteria=[ + EvalCriterion(name="file_created", instruction="API file was created"), + EvalCriterion(name="has_crud", instruction="Contains GET, POST, DELETE endpoints"), + ], + metadata={"category": "medium"}, + ), + Task( + name="data_analysis", + prompt="Create a Python script that generates 100 random data points, calculates mean/median/std, and saves results to stats.json.", + criteria=[ + EvalCriterion(name="script_created", instruction="Python script was created"), + EvalCriterion(name="json_output", instruction="stats.json was created with results"), + ], + metadata={"category": "medium"}, + ), + Task( + name="cli_tool", + prompt="Create a CLI tool using argparse that takes a filename and counts lines, words, and characters (like wc).", + criteria=[ + EvalCriterion(name="file_created", instruction="CLI script was created"), + EvalCriterion(name="uses_argparse", instruction="Uses argparse for argument parsing"), + ], + metadata={"category": "medium"}, + ), + Task( + name="unit_tests", + prompt="Create a calculator module (calc.py) with add/subtract/multiply/divide functions, then write pytest tests for it (test_calc.py).", + criteria=[ + EvalCriterion(name="module_created", instruction="calc.py was created"), + EvalCriterion(name="tests_created", instruction="test_calc.py was created"), + EvalCriterion(name="tests_pass", instruction="Tests pass when run"), + ], + metadata={"category": "medium"}, + ), + ], + "coding": [ + Task( + name="fizzbuzz", + prompt="Create fizzbuzz.py that prints FizzBuzz 1-100 and run it.", + criteria=[EvalCriterion(name="correct", instruction="Correct FizzBuzz output")], + metadata={"category": "short"}, + ), + Task( + name="rest_api", + prompt="Create a FastAPI CRUD TODO app with GET/POST/DELETE endpoints.", + criteria=[EvalCriterion(name="has_crud", instruction="Has working CRUD")], + metadata={"category": "medium"}, + ), + Task( + name="cli_tool", + prompt="Create an argparse CLI that counts lines/words/chars in a file.", + criteria=[EvalCriterion(name="works", instruction="CLI works correctly")], + metadata={"category": "medium"}, + ), + Task( + name="data_pipeline", + prompt="Create a script that reads CSV data, filters rows, aggregates, and outputs JSON.", + criteria=[EvalCriterion(name="works", instruction="Pipeline produces correct output")], + metadata={"category": "medium"}, + ), + Task( + name="unit_tests", + prompt="Create calc.py with math functions and test_calc.py with pytest tests.", + criteria=[EvalCriterion(name="tests_pass", instruction="Tests pass")], + metadata={"category": "medium"}, + ), + Task( + name="web_scraper", + prompt="Create a script that fetches a webpage and extracts all links.", + criteria=[EvalCriterion(name="extracts_links", instruction="Extracts links correctly")], + metadata={"category": "medium"}, + ), + Task( + name="async_downloader", + prompt="Create an async script that downloads multiple URLs concurrently using aiohttp.", + criteria=[EvalCriterion(name="uses_async", instruction="Uses async/await correctly")], + metadata={"category": "complex"}, + ), + Task( + name="database_orm", + prompt="Create a SQLAlchemy model for Users with CRUD operations.", + criteria=[EvalCriterion(name="has_orm", instruction="Uses SQLAlchemy ORM correctly")], + metadata={"category": "complex"}, + ), + Task( + name="decorator_lib", + prompt="Create a library with timing, retry, and caching decorators.", + criteria=[EvalCriterion(name="decorators_work", instruction="Decorators function correctly")], + metadata={"category": "complex"}, + ), + Task( + name="config_parser", + prompt="Create a config parser that supports YAML, JSON, and env vars with validation.", + criteria=[EvalCriterion(name="multi_format", instruction="Supports multiple formats")], + metadata={"category": "complex"}, + ), + ], +} + + +def get_task_suite(suite_name: str) -> list[Task]: + """Get a built-in task suite by name. + + Args: + suite_name: Name of the suite ('quick', 'core', 'coding') + + Returns: + List of Task objects + + Raises: + ValueError: If suite_name is not found + """ + if suite_name not in TASK_SUITES: + available = ", ".join(TASK_SUITES.keys()) + raise ValueError(f"Unknown suite '{suite_name}'. Available: {available}") + return TASK_SUITES[suite_name] diff --git a/src/flow/harness/__init__.py b/src/flow/harness/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4e359ad37197f2deef797f2ebb8484ba071f30f6 --- /dev/null +++ b/src/flow/harness/__init__.py @@ -0,0 +1,18 @@ +"""Harness modules for Flow agent. + +Harnesses are agent runtime adapters that convert different agent framework +events to a uniform Event format for CLI/UI consumption. + +Available harnesses: +- maf: Microsoft Agent Framework harness +- (future) langchain: LangChain harness +- (future) claude: Claude SDK harness +""" + +from flow.harness.base import BaseHarness, Event, EventType + +__all__ = [ + "BaseHarness", + "Event", + "EventType", +] diff --git a/src/flow/harness/base.py b/src/flow/harness/base.py new file mode 100644 index 0000000000000000000000000000000000000000..3d8e985a177afa515c245041cd6fb1aa99fd0512 --- /dev/null +++ b/src/flow/harness/base.py @@ -0,0 +1,110 @@ +"""Base harness interface for agent runtimes. + +Defines the abstract interface that all harnesses must implement, +allowing Flow to run on different agent frameworks. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import AsyncIterator, Callable, Coroutine +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + + +class EventType(Enum): + """Types of events that can be streamed from an agent.""" + + TEXT_DELTA = "text_delta" # Streaming text chunk + TEXT_DONE = "text_done" # Text generation complete + TOOL_CALL_START = "tool_call_start" # Starting a tool call + TOOL_CALL_ARGS = "tool_call_args" # Tool call arguments (streaming) + TOOL_CALL_DONE = "tool_call_done" # Tool call complete + TOOL_RESULT = "tool_result" # Tool execution result + THINKING = "thinking" # Agent reasoning/thinking + ERROR = "error" # An error occurred + DONE = "done" # Agent run complete + + +@dataclass +class Event: + """An event from the agent execution stream. + + Events provide real-time feedback during agent execution, + allowing the CLI to display progress, tool calls, and results. + """ + + type: EventType + content: str = "" + tool_name: str | None = None + tool_call_id: str | None = None + metadata: dict[str, str | int | float | bool | None] = field(default_factory=dict) + + +class BaseHarness(ABC): + """Abstract base class for agent execution harnesses. + + A harness is a thin adapter that converts agent framework events + to the uniform Flow Event format for CLI/UI consumption. + + Each harness implementation handles: + - Taking a pre-configured agent from the framework + - Running tasks on the agent + - Converting framework-specific events to Flow Events + - Managing conversation threads + + Implementations: + - MAFHarness (flow.harness.maf): Microsoft Agent Framework + - (Future) LangChainHarness: LangChain + - (Future) ClaudeHarness: Claude SDK + """ + + @abstractmethod + async def run(self, task: str, thread_id: str | None = None) -> str: + """Run a task and return the final response. + + Args: + task: The task/prompt to execute + thread_id: Optional thread ID for conversation continuity + + Returns: + The agent's final response text + """ + ... + + @abstractmethod + def run_stream(self, task: str, thread_id: str | None = None) -> AsyncIterator[Event]: + """Run a task with streaming events. + + Args: + task: The task/prompt to execute + thread_id: Optional thread ID for conversation continuity + + Yields: + Event objects representing agent activity + """ + ... + + @abstractmethod + def register_tools(self, tools: list[Callable[..., Coroutine[Any, Any, str]]]) -> None: + """Register tools with the harness. + + Args: + tools: List of tool functions to register + """ + ... + + @abstractmethod + def get_thread_id(self) -> str: + """Get the current thread ID. + + Returns: + The current conversation thread ID + """ + ... + + @abstractmethod + async def close(self) -> None: + """Clean up resources used by the harness.""" + ... diff --git a/src/flow/harness/maf/__init__.py b/src/flow/harness/maf/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bc97a674c16c3be0539d850a182267eb56158020 --- /dev/null +++ b/src/flow/harness/maf/__init__.py @@ -0,0 +1,14 @@ +"""Microsoft Agent Framework harness module. + +Provides integration with Microsoft Agent Framework for running Flow agents. +""" + +from flow.harness.maf.agent import create_agent +from flow.harness.maf.harness import MAFHarness +from flow.harness.maf.message_store import HeadTailCompactingChatMessageStore + +__all__ = [ + "create_agent", + "HeadTailCompactingChatMessageStore", + "MAFHarness", +] diff --git a/src/flow/harness/maf/agent.py b/src/flow/harness/maf/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..6ada049e4b63527a6217106a0c8da212ada2a8fa --- /dev/null +++ b/src/flow/harness/maf/agent.py @@ -0,0 +1,176 @@ +"""Agent factory for Microsoft Agent Framework. + +Provides factory functions to create configured ChatAgent instances. +""" + +import logging +import os +from collections.abc import Callable, Coroutine, Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from flow.harness.maf.message_store import HeadTailCompactingChatMessageStore +from flow.prompts import FLOW_AGENT_INSTRUCTIONS +from flow.tools import create_all_tools + +if TYPE_CHECKING: + from agent_framework import ChatAgent + +logger = logging.getLogger(__name__) + +# Default paths +DEFAULT_WORKSPACE = Path.home() / ".flow" / "workspace" +DEFAULT_MEMORY_PATH = Path.home() / ".flow" / "memory" + + +def create_agent( + *, + # Model/API configuration + endpoint: str | None = None, + api_key: str | None = None, + deployment: str | None = None, + api_version: str = "2024-02-15-preview", + # Agent configuration + name: str = "Flow", + instructions: str | None = None, + # Workspace configuration + workspace: Path | None = None, + memory_path: Path | None = None, + # Tool configuration + tools: Sequence[Callable[..., Coroutine[Any, Any, str]]] | None = None, + enable_memory_tool: bool = True, + enable_sub_agent: bool = False, + bash_timeout: int = 120, + # Context engineering + enable_compaction: bool = True, + compaction_head_size: int = 10, + compaction_tail_size: int = 40, +) -> "ChatAgent": + """Create a configured ChatAgent for Flow. + + This factory creates a Microsoft Agent Framework ChatAgent with: + - Azure OpenAI as the backend + - Flow's standard tools (coding, execution, memory) + - Optional message compaction for long conversations + - Optional agent-managed memory tool + - Optional sub-agent for isolated research + + Args: + endpoint: Azure OpenAI endpoint URL. Defaults to AZURE_OPENAI_ENDPOINT env var. + api_key: Azure OpenAI API key. Defaults to AZURE_OPENAI_API_KEY env var. + deployment: Azure OpenAI deployment name. Defaults to AZURE_OPENAI_DEPLOYMENT env var. + api_version: Azure OpenAI API version. + name: Agent name. + instructions: Agent instructions. Defaults to FLOW_AGENT_INSTRUCTIONS. + workspace: Directory for file operations. Defaults to ~/.flow/workspace. + memory_path: Directory for persistent memory. Defaults to ~/.flow/memory. + tools: Custom tools to use. If None, creates standard Flow tools. + enable_memory_tool: Whether to include the memory tool (default: True). + enable_sub_agent: Whether to include the sub-agent tool (default: False). + bash_timeout: Timeout for bash commands in seconds. + enable_compaction: Whether to enable head+tail message compaction. + compaction_head_size: Number of initial messages to keep. + compaction_tail_size: Number of recent messages to keep. + + Returns: + Configured ChatAgent instance. + + Raises: + ImportError: If agent_framework is not installed. + ValueError: If required Azure OpenAI credentials are missing. + + Example: + >>> from flow.harness.maf import create_agent + >>> agent = create_agent() + >>> thread = agent.get_new_thread() + >>> response = await agent.run("Create a hello world script", thread=thread) + """ + try: + from agent_framework import ChatAgent, ai_function + from agent_framework.azure import AzureOpenAIChatClient + except ImportError as e: + raise ImportError( + "Microsoft Agent Framework is required. " + "Install with: pip install agent-framework-core" + ) from e + + # Resolve configuration from environment if not provided + endpoint = endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT") + api_key = api_key or os.environ.get("AZURE_OPENAI_API_KEY") + deployment = deployment or os.environ.get("AZURE_OPENAI_DEPLOYMENT") + + if not endpoint: + raise ValueError( + "Azure OpenAI endpoint is required. " + "Set AZURE_OPENAI_ENDPOINT or pass endpoint parameter." + ) + if not api_key: + raise ValueError( + "Azure OpenAI API key is required. " + "Set AZURE_OPENAI_API_KEY or pass api_key parameter." + ) + if not deployment: + raise ValueError( + "Azure OpenAI deployment is required. " + "Set AZURE_OPENAI_DEPLOYMENT or pass deployment parameter." + ) + + # Resolve paths + workspace = workspace or DEFAULT_WORKSPACE + memory_path = memory_path or DEFAULT_MEMORY_PATH + + # Ensure directories exist + workspace.mkdir(parents=True, exist_ok=True) + memory_path.mkdir(parents=True, exist_ok=True) + + # Create or use provided tools + if tools is None: + tools = create_all_tools( + workspace=workspace, + memory_path=memory_path, + bash_timeout=bash_timeout, + enable_memory_tool=enable_memory_tool, + enable_sub_agent=enable_sub_agent, + ) + + # Wrap tools with ai_function decorator for Agent Framework + converted_tools = [] + for tool_func in tools: + tool_name = getattr(tool_func, "_tool_name", tool_func.__name__) + tool_description = getattr(tool_func, "_tool_description", tool_func.__doc__ or "") + wrapped = ai_function(name=tool_name, description=tool_description)(tool_func) + converted_tools.append(wrapped) + + # Create the chat client + client = AzureOpenAIChatClient( + api_key=api_key, + endpoint=endpoint, + deployment=deployment, + api_version=api_version, + ) + + # Create message store factory if compaction is enabled + message_store_factory = None + if enable_compaction: + def create_compacting_store() -> HeadTailCompactingChatMessageStore: + return HeadTailCompactingChatMessageStore( + head_size=compaction_head_size, + tail_size=compaction_tail_size, + ) + + message_store_factory = create_compacting_store + logger.debug( + f"Message compaction enabled: head={compaction_head_size}, tail={compaction_tail_size}" + ) + + # Create the agent + agent = ChatAgent( + name=name, + description="Autonomous coding agent", + instructions=instructions or FLOW_AGENT_INSTRUCTIONS, + chat_client=client, + tools=converted_tools, + chat_message_store_factory=message_store_factory, + ) + + return agent diff --git a/src/flow/harness/maf/harness.py b/src/flow/harness/maf/harness.py new file mode 100644 index 0000000000000000000000000000000000000000..ec18e713d16376497f95602ab005cb94759984a2 --- /dev/null +++ b/src/flow/harness/maf/harness.py @@ -0,0 +1,258 @@ +"""Microsoft Agent Framework harness. + +A thin adapter that converts Agent Framework events to the uniform Flow Event format. +""" + +import logging +import uuid +from collections.abc import AsyncIterator +from typing import TYPE_CHECKING, Any + +from flow.harness.base import BaseHarness, Event, EventType + +if TYPE_CHECKING: + from agent_framework import ChatAgent + +logger = logging.getLogger(__name__) + +# Track if instrumentation has been enabled globally +_instrumentation_enabled = False + + +def _enable_instrumentation() -> None: + """Enable OpenTelemetry instrumentation for Agent Framework. + + This is called once when the first harness is created. + Instrumentation allows trace collection for experiments. + """ + global _instrumentation_enabled + if _instrumentation_enabled: + return + + try: + from agent_framework.observability import enable_instrumentation + enable_instrumentation() + _instrumentation_enabled = True + logger.debug("Agent Framework instrumentation enabled") + except ImportError: + logger.debug("Agent Framework observability not available") + except Exception as e: + logger.debug(f"Could not enable instrumentation: {e}") + + +class MAFHarness(BaseHarness): + """Harness adapter for Microsoft Agent Framework. + + This adapter: + 1. Takes a ChatAgent (or creates one with default settings) + 2. Runs tasks on the agent + 3. Converts Agent Framework events to uniform Flow Events + + Example: + >>> from flow.harness.maf import MAFHarness + >>> # Simple usage - creates agent with defaults + >>> harness = MAFHarness() + >>> async for event in harness.run_stream("Create a hello world script"): + ... print(event) + + >>> # Or with custom agent + >>> from flow.harness.maf import create_agent + >>> agent = create_agent(enable_compaction=False) + >>> harness = MAFHarness(agent) + """ + + def __init__( + self, + agent: "ChatAgent | None" = None, + **create_agent_kwargs: Any, + ) -> None: + """Initialize the harness. + + Args: + agent: Optional ChatAgent instance. If not provided, creates one + using create_agent() with the given kwargs. + **create_agent_kwargs: Passed to create_agent() if agent is None. + Common options: workspace, memory_path, + enable_compaction, enable_memory_tool. + """ + if agent is None: + from flow.harness.maf.agent import create_agent + agent = create_agent(**create_agent_kwargs) + self._agent: ChatAgent = agent # type: ignore[assignment] + self._thread: Any = None # AgentThread for conversation continuity + self._thread_id: str | None = None + # Track tool calls we've seen to avoid duplicate TOOL_CALL_START events + self._seen_tool_calls: set[str] = set() + + # Enable OpenTelemetry instrumentation for trace collection + _enable_instrumentation() + + def register_tools(self, tools: list[Any]) -> None: + """Register tools with the harness. + + Note: For MAFHarness, tools should be configured when creating the agent + via create_agent(). This method is provided for interface compatibility + but will log a warning if called. + + Args: + tools: List of tool functions (ignored - configure via create_agent) + """ + logger.warning( + "MAFHarness.register_tools() called but tools should be configured " + "via create_agent(). These tools will be ignored." + ) + + async def run(self, task: str, thread_id: str | None = None) -> str: + """Run a task and return the final response. + + Args: + task: The task/prompt to execute + thread_id: Optional thread ID for conversation continuity + + Returns: + The agent's final response text + """ + if thread_id: + self._thread_id = thread_id + + # Get or create an AgentThread for conversation continuity + if self._thread is None: + self._thread = self._agent.get_new_thread() + + response = await self._agent.run(task, thread=self._thread) + + # Extract text content from response + content = getattr(response, "content", None) + if content is not None: + return str(content) + return str(response) + + async def run_stream( + self, task: str, thread_id: str | None = None + ) -> AsyncIterator[Event]: + """Run a task with streaming events. + + Args: + task: The task/prompt to execute + thread_id: Optional thread ID for conversation continuity + + Yields: + Event objects representing agent activity + """ + if thread_id: + self._thread_id = thread_id + + # Get or create an AgentThread for conversation continuity + if self._thread is None: + self._thread = self._agent.get_new_thread() + + # Clear seen tool calls for this run + self._seen_tool_calls.clear() + + try: + # Check if agent supports streaming + if hasattr(self._agent, "run_stream"): + async for chunk in self._agent.run_stream(task, thread=self._thread): + # Convert agent_framework events to Flow events + events = self._convert_event(chunk) + for event in events: + yield event + else: + # Fallback: run non-streaming and emit single event + response = await self._agent.run(task, thread=self._thread) + response_content = getattr(response, "content", None) + content = str(response_content) if response_content is not None else str(response) + yield Event(type=EventType.TEXT_DONE, content=content) + + yield Event(type=EventType.DONE) + + except Exception as e: + yield Event(type=EventType.ERROR, content=str(e)) + + def _convert_event(self, chunk: Any) -> list[Event]: + """Convert an agent_framework event to Flow Events. + + Args: + chunk: Event from agent_framework (AgentResponseUpdate) + + Returns: + List of converted Events (may be empty) + """ + events: list[Event] = [] + chunk_type = type(chunk).__name__ + + # AgentResponseUpdate/AgentRunResponseUpdate has .contents list and .text property + if chunk_type in ("AgentResponseUpdate", "AgentRunResponseUpdate") or hasattr(chunk, "contents"): + contents = getattr(chunk, "contents", []) or [] + + for content in contents: + content_type = type(content).__name__ + + if content_type == "TextContent": + text = getattr(content, "text", "") + if text: + events.append(Event(type=EventType.TEXT_DELTA, content=text)) + + elif content_type == "FunctionCallContent": + # Streaming pattern: + # - First chunk has call_id and name set, arguments='' + # - Subsequent chunks have empty call_id/name, just argument fragments + call_id = getattr(content, "call_id", "") or "" + name = getattr(content, "name", "") or "" + args = getattr(content, "arguments", "") or "" + + if call_id and name: + # First chunk - emit TOOL_CALL_START + self._seen_tool_calls.add(call_id) + events.append(Event( + type=EventType.TOOL_CALL_START, + tool_name=name, + tool_call_id=call_id, + )) + elif args: + # Argument fragment - emit as TOOL_CALL_ARGS + events.append(Event( + type=EventType.TOOL_CALL_ARGS, + content=args, + )) + + elif content_type == "FunctionResultContent": + result = getattr(content, "result", "") + call_id = getattr(content, "call_id", None) + events.append(Event( + type=EventType.TOOL_RESULT, + content=str(result), + tool_call_id=call_id, + )) + # Emit TOOL_CALL_DONE after result + events.append(Event(type=EventType.TOOL_CALL_DONE)) + + # If no contents but has text, use that + if not events and hasattr(chunk, "text"): + text = chunk.text + if text: + events.append(Event(type=EventType.TEXT_DELTA, content=text)) + + # Fallback for other chunk types + elif hasattr(chunk, "text"): + text = chunk.text + if text: + events.append(Event(type=EventType.TEXT_DELTA, content=text)) + + return events + + def get_thread_id(self) -> str: + """Get the current thread ID. + + Returns: + The current conversation thread ID + """ + if self._thread_id is None: + self._thread_id = str(uuid.uuid4()) + return self._thread_id + + async def close(self) -> None: + """Clean up resources used by the harness.""" + # Agent Framework doesn't require explicit cleanup + self._thread = None + self._thread_id = None diff --git a/src/flow/harness/maf/message_store.py b/src/flow/harness/maf/message_store.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ad9581f6d168e360f9ee5a9146db5fb0d97daa --- /dev/null +++ b/src/flow/harness/maf/message_store.py @@ -0,0 +1,177 @@ +"""Message store implementations for Microsoft Agent Framework. + +Provides ChatMessageStoreProtocol implementations for context management. +""" + +from collections.abc import MutableMapping, Sequence +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from agent_framework import ChatMessage + + +class HeadTailCompactingChatMessageStore: + """A compacting message store that works directly with Agent Framework ChatMessage. + + This store implements ChatMessageStoreProtocol and keeps the first N messages + (head) and last M messages (tail), dropping middle messages to prevent + context overflow in long conversations. + + IMPORTANT: This store preserves full ChatMessage objects including: + - FunctionCallContent (tool calls) + - FunctionResultContent (tool results) + - All other content types + + This is critical because OpenAI's API requires tool results to immediately + follow their corresponding tool calls. + + The compaction strategy: + - Keeps the first N messages (task context, initial instructions) + - Keeps the last M messages (recent work, current state) + - Drops middle messages to prevent context overflow + """ + + def __init__( + self, + messages: Sequence["ChatMessage"] | None = None, + head_size: int = 10, + tail_size: int = 40, + ) -> None: + """Initialize the compacting store. + + Args: + messages: Initial messages to store + head_size: Number of initial messages to keep + tail_size: Number of recent messages to keep + """ + if head_size < 0: + raise ValueError("head_size must be non-negative") + if tail_size < 0: + raise ValueError("tail_size must be non-negative") + + self._messages: list["ChatMessage"] = list(messages) if messages else [] + self._head_size = head_size + self._tail_size = tail_size + + @property + def head_size(self) -> int: + """Number of messages kept from the beginning.""" + return self._head_size + + @property + def tail_size(self) -> int: + """Number of messages kept from the end.""" + return self._tail_size + + @property + def total_messages(self) -> int: + """Total number of messages stored (before compaction).""" + return len(self._messages) + + @property + def compacted_count(self) -> int: + """Number of messages that would be returned by list_messages().""" + total = len(self._messages) + max_kept = self._head_size + self._tail_size + return min(total, max_kept) + + @property + def dropped_count(self) -> int: + """Number of messages dropped during compaction.""" + return max(0, self.total_messages - self.compacted_count) + + async def add_messages(self, messages: Sequence["ChatMessage"]) -> None: + """Add messages to the store. + + Messages are stored as-is, preserving all content types. + + Args: + messages: Sequence of ChatMessage objects to add + """ + self._messages.extend(messages) + + async def list_messages(self) -> list["ChatMessage"]: + """Get messages with head+tail compaction applied. + + Returns the first head_size messages plus the last tail_size messages. + If total messages <= head_size + tail_size, returns all messages. + + Returns: + List of ChatMessage objects after compaction + """ + total = len(self._messages) + max_kept = self._head_size + self._tail_size + + # No compaction needed + if total <= max_kept: + return list(self._messages) + + # Return head + tail + head = self._messages[: self._head_size] + tail = self._messages[-self._tail_size :] if self._tail_size > 0 else [] + + return head + tail + + @classmethod + async def deserialize( + cls, + serialized_store_state: MutableMapping[str, Any], + **kwargs: Any, + ) -> "HeadTailCompactingChatMessageStore": + """Create store from serialized state.""" + from agent_framework import ChatMessage + + head_size = kwargs.get("head_size", serialized_store_state.get("head_size", 10)) + tail_size = kwargs.get("tail_size", serialized_store_state.get("tail_size", 40)) + + messages_data = serialized_store_state.get("messages", []) + messages = [ + ChatMessage.from_dict(m) if isinstance(m, dict) else m + for m in messages_data + ] + + return cls(messages=messages, head_size=head_size, tail_size=tail_size) + + async def update_from_state( + self, + serialized_store_state: MutableMapping[str, Any], + **kwargs: Any, + ) -> None: + """Update store from serialized state.""" + from agent_framework import ChatMessage + + if not serialized_store_state: + return + + messages_data = serialized_store_state.get("messages", []) + self._messages = [ + ChatMessage.from_dict(m) if isinstance(m, dict) else m + for m in messages_data + ] + + if "head_size" in serialized_store_state: + self._head_size = serialized_store_state["head_size"] + if "tail_size" in serialized_store_state: + self._tail_size = serialized_store_state["tail_size"] + + async def serialize(self, **kwargs: Any) -> dict[str, Any]: + """Serialize the store state. + + Serializes ALL messages (not just compacted view) plus configuration. + """ + return { + "messages": [m.to_dict() for m in self._messages], + "head_size": self._head_size, + "tail_size": self._tail_size, + } + + @property + def stats(self) -> dict[str, int]: + """Get compaction statistics.""" + return { + "total_messages": self.total_messages, + "compacted_count": self.compacted_count, + "dropped_count": self.dropped_count, + "head_size": self._head_size, + "tail_size": self._tail_size, + } diff --git a/src/flow/prompts.py b/src/flow/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..0dbbd3f2ecfaec2a0f8ee6ce123bec48bc7e9501 --- /dev/null +++ b/src/flow/prompts.py @@ -0,0 +1,407 @@ +"""System prompts for the Flow agent. + +Defines the structured workflow for software engineering tasks. +""" + +FLOW_AGENT_INSTRUCTIONS = """ +You are an expert autonomous agent. You solve problems end-to-end by composing your available tools. + +## CORE PRINCIPLE: BE AUTONOMOUS + +**You are NOT just an assistant that tells users what to do. You ARE the one who does it.** + +When asked to solve a task: +1. **DO IT YOURSELF** - Don't tell the user to run commands. Run them yourself. +2. **COMPLETE THE LOOP** - Write code AND execute it. Don't stop at writing. +3. **VERIFY YOUR WORK** - Test that it actually works before reporting done. +4. **ITERATE ON FAILURES** - If something fails, fix it and try again. + +**Example - BAD (passive):** +> "Here's the code. You can run it with `python script.py`" + +**Example - GOOD (autonomous):** +> *writes code* β†’ *executes code* β†’ *sees output* β†’ *fixes any errors* +> β†’ "Done! The script ran successfully and output X." + +--- + +## YOUR CAPABILITIES + +**Coding Tools:** +- `read_file`: Read file contents with line numbers +- `write_file`: Create/edit files (full write, str_replace, or insert_at_line) +- `list_directory`: Explore project structure +- `grep_search`: Search for patterns in code (regex supported) + +**Execution Tools:** +- `bash_execute`: Run shell commands (tests, git, npm, pip, builds, etc.) +- `python_repl`: Execute Python code snippets for quick validation + +**Research Tools (if available):** +- `web_search`: Search the web using Google (requires GOOGLE_API_KEY and GOOGLE_CSE_ID) +- `web_fetch`: Fetch and read content from URLs + +**Memory Tools:** +- `memory`: Persistent storage that survives across conversations + - view: See directory or file contents + - create: Create new files + - str_replace: Edit existing files + - append: Add to files + - search: Find text across memory + - delete: Remove files + +**Thinking Tools:** +- `think`: Pause to reason through complex problems +- `task_done`: Report when task is complete or blocked + +**Skills Tool (if available):** +- `skills`: Discover and load domain-specific expertise + - `skills(action='list')`: See available skills with descriptions + - `skills(action='load', name='skill-name')`: Load full skill content + +--- + +## WORKFLOW + +### 1. UNDERSTAND +- Read the user's request carefully +- **If the `skills` tool is available**, call `skills(action='list')` to discover relevant expertise +- Use `list_directory` to understand the workspace structure +- Use `grep_search` to find relevant existing code +- Check memory for relevant patterns: `memory(command="view", path="/memory")` + +### 2. PLAN +- Use `think` tool to plan your approach for complex tasks +- Break down into small, testable steps +- Consider edge cases and error handling + +### 3. EXECUTE +- Create/edit files using `write_file` +- Test changes using `bash_execute` or `python_repl` +- Fix issues immediately when tests fail + +### 4. VERIFY (REQUIRED) +**You MUST test your work before calling `task_done`.** Never assume code works. + +**For Python apps/scripts:** +``` +bash_execute("cd project && python -c 'import main'") # Check imports work +bash_execute("cd project && python main.py --help") # Test CLI if applicable +bash_execute("cd project && pytest") # Run tests if they exist +``` + +**For JavaScript/TypeScript:** +``` +bash_execute("cd project && npm install && npm run build") # Must pass! +bash_execute("cd project && npx tsc --noEmit") # Type check +``` + +**For Web APIs (FastAPI, Express, etc.):** +``` +# Start server in background, test with curl, then cleanup +bash_execute("cd project && uvicorn main:app --port 8000 &", background=True) +bash_execute("sleep 2 && curl http://localhost:8000/health") # Test endpoint +bash_execute("check_processes action=list") # Verify it's running +# When done testing, kill the process +``` + +**For Frontend apps (React, Vue, etc.):** +``` +bash_execute("cd project && npm run build") # Production build must succeed +# If you need to test dev server, use background=True +``` + +**For full-stack apps:** +1. Test backend API with curl (start in background) +2. Test frontend build succeeds +3. Clean up background processes when done + +### 5. COMPLETE +- Clean up any background processes you started +- Call `task_done` with status and summary +- Include files created and suggested next steps + +--- + +## WORKSPACE + +Your workspace is at `~/.flow/workspace/` + +**Organization:** +- Create a folder for each project (e.g., `todo_app/`, `calculator/`) +- Use `list_directory` to see existing projects before creating new ones +- Follow standard project structure conventions: + - Python: `src/`, `tests/`, `requirements.txt` or `pyproject.toml` + - JavaScript: `src/`, `package.json`, standard Node.js layout + - Full-stack: `backend/`, `frontend/` folders + +**Important:** +- Each `bash_execute` runs from workspace root in a fresh shell +- Use `cd project && command` for commands in subdirectories +- Multiple commands: `cd project && cmd1 && cmd2` + +--- + +## MEMORY + +Your memory persists at `~/.flow/memory/` + +**Recommended structure:** +- `/memory/patterns/` - Reusable solutions and code patterns +- `/memory/projects/` - Per-project context and notes +- `/memory/decisions/` - Why you made certain choices + +**Best practices:** +When storing information, include context: +- **Date**: When was this created/learned? +- **Project**: What project did this come from? +- **Context**: Why was this approach chosen? + +**Example pattern file** (`/memory/patterns/fastapi_cors.md`): +```markdown +# FastAPI CORS Setup +Created: 2025-01-15 +Source: sleep_tracker project + +## Pattern +from fastapi.middleware.cors import CORSMiddleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +## When to use +- Full-stack apps with separate frontend/backend +- Frontend on different port than backend + +## Notes +- Must add before routes +- Restrict origins in production +``` + +**Check memory first** - you may have solved similar problems before! + +--- + +## CLI TOOLS + +Many CLI tools have interactive prompts that will hang. +ALWAYS use non-interactive flags: + +```bash +# Good +npm create vite@latest myapp -- --template react-ts +pip install -q package +npx shadcn@latest init --defaults --yes + +# Bad (will hang) +npm create vite@latest myapp # Interactive prompts +npx shadcn init # Interactive prompts +``` + +**Shadcn UI** is a CLI tool, not an npm package: +```bash +# Wrong +npm install @shadcn/ui + +# Right +npx shadcn@latest init --defaults --yes +npx shadcn@latest add button card --yes +``` + +--- + +## FULL-STACK APPS + +When building apps with separate frontend and backend: + +1. **Always add CORS to backend:** +```python +from fastapi.middleware.cors import CORSMiddleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Restrict in production + allow_methods=["*"], + allow_headers=["*"], +) +``` + +2. **Document which ports each server uses** + +3. **Verify both sides build/run:** +```bash +cd backend && python -c "from main import app; print('Backend OK')" +cd frontend && npm run build && echo "Frontend OK" +``` + +--- + +## BACKGROUND PROCESSES + +When you need to start long-running processes (servers, watchers, etc.): + +**Use `background=True` parameter:** +```python +# Start a server in background - returns immediately with PID +bash_execute("uvicorn main:app --port 8000", background=True) + +# Then test it +bash_execute("curl http://localhost:8000/health") + +# Check what's running +check_processes(action="list") + +# Clean up when done +check_processes(action="kill", pid=12345) +``` + +**Process registry** is at `/memory/processes.md` - view it with: +`memory(command='view', path='/memory/processes.md')` + +**IMPORTANT:** +- NEVER start servers without `background=True` - they will timeout after 120s +- ALWAYS clean up background processes when done testing +- Check for port conflicts before starting servers + +**Common patterns:** +```bash +# Good - background server for testing +bash_execute("cd backend && uvicorn main:app --port 8000", background=True) +bash_execute("sleep 2") # Wait for startup +bash_execute("curl localhost:8000/docs") # Test +check_processes(action="cleanup") # Kill all when done + +# Bad - will timeout! +bash_execute("uvicorn main:app --port 8000") # Blocks forever +``` + +--- + +## ERROR HANDLING + +- If a command fails, analyze the error and try alternatives +- Log failures and solutions to memory for future reference +- Don't give up after first failure - iterate +- If truly blocked, call `task_done` with status="incomplete" and explain why + +--- + +## SKILLS + +**If the `skills` tool is available**, use it to access domain-specific expertise: + +```python +# At the start of complex tasks, discover what expertise is available +skills(action='list') + +# Output shows available skills with descriptions: +# - fastapi-patterns: Build REST APIs with FastAPI... +# - react-components: Build React components with hooks... +# - testing-strategies: Write comprehensive tests... + +# Load relevant skills before implementation +skills(action='load', name='fastapi-patterns') +``` + +**Skills provide:** +- Domain-specific patterns and best practices +- Code examples and templates +- Common pitfalls to avoid + +**When to load skills:** +- Before starting a new project type (API, frontend, CLI) +- When working with unfamiliar frameworks +- For complex tasks requiring specialized knowledge + +**Skills location:** `~/.flow/skills/` +Each skill is a folder with a `SKILL.md` file following the Anthropic Skills standard. + +--- + +## COMPOSING TOOLS FOR COMPLEX TASKS + +**You have all the tools needed to solve problems end-to-end. Compose them!** + +### Example: "What's the weather API response for Seattle?" +``` +# DON'T just tell the user how to do it. DO IT: +1. web_search("weather API free") β†’ Find a free weather API +2. web_fetch(api_docs_url) β†’ Read the API documentation +3. write_file("weather.py", code) β†’ Write a script to call the API +4. bash_execute("python weather.py") β†’ Run it and get the answer +5. Report the actual result to the user +``` + +### Example: "Create a CLI tool that converts CSV to JSON" +``` +1. write_file("csv_to_json.py", code) β†’ Write the tool +2. write_file("test.csv", sample_data) β†’ Create test data +3. bash_execute("python csv_to_json.py test.csv") β†’ Test it works +4. bash_execute("cat output.json") β†’ Verify the output +5. Report success with example output +``` + +### Example: "Find and summarize the latest Python 3.12 features" +``` +1. web_search("Python 3.12 new features") β†’ Find relevant pages +2. web_fetch(python_docs_url) β†’ Read the official docs +3. Summarize findings directly OR write to a file if requested +``` + +### Example: "Debug why my FastAPI app returns 500 errors" +``` +1. read_file("main.py") β†’ Understand the code +2. bash_execute("cd app && python -c 'from main import app'") β†’ Check imports +3. bash_execute("cd app && uvicorn main:app --port 8000", background=True) β†’ Start server +4. bash_execute("curl localhost:8000/endpoint") β†’ Reproduce the error +5. Analyze error β†’ Fix code β†’ Test again β†’ Iterate until fixed +``` + +--- + +## RESEARCH WORKFLOW + +When you need information from the web: + +1. **Search first**: Use `web_search` to find relevant URLs +2. **Fetch details**: Use `web_fetch` to read specific pages +3. **Apply knowledge**: Write code, update configs, or summarize findings + +**Example - Learning a new library:** +```python +# 1. Search for docs +web_search("httpx python async http client tutorial") + +# 2. Read the documentation +web_fetch("https://www.python-httpx.org/quickstart/", output_format="markdown") + +# 3. Write code using what you learned +write_file("http_client.py", ''' +import httpx +async def fetch_data(url): + async with httpx.AsyncClient() as client: + return await client.get(url) +''') + +# 4. Test it +python_repl("import httpx; print(httpx.__version__)") +``` + +--- + +## REMEMBER + +1. **BE AUTONOMOUS** - Do the work yourself, don't instruct the user +2. **COMPLETE THE LOOP** - Write code β†’ Execute β†’ Verify β†’ Report results +3. **COMPOSE TOOLS** - Chain multiple tools to solve complex problems +4. **RESEARCH WHEN NEEDED** - Use web_search/web_fetch to learn new things +5. **ITERATE ON FAILURES** - Don't give up, debug and fix issues +6. **TEST EVERYTHING** - Never assume code works +7. **USE NON-INTERACTIVE FLAGS** - Avoid hanging commands +8. **CLEAN UP** - Kill background processes when done +9. **STORE LEARNINGS** - Save patterns to memory for future use + +**Your goal is to deliver RESULTS, not instructions.** +""" diff --git a/src/flow/py.typed b/src/flow/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/flow/tools/__init__.py b/src/flow/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4c00cb66edfa50accb5ceefc03114b1f0dc37212 --- /dev/null +++ b/src/flow/tools/__init__.py @@ -0,0 +1,172 @@ +"""Flow agent tools. + +Provides coding, execution, memory, and core tools for software engineering tasks. +Tools are harness-agnostic - they return plain data that harnesses adapt. +""" + +import inspect +from collections.abc import Callable, Sequence +from functools import wraps +from pathlib import Path +from typing import Any, get_type_hints + +from flow.tools.coding import create_coding_tools +from flow.tools.core import create_core_tools +from flow.tools.execution import create_execution_tools +from flow.tools.memory import create_memory_tool +from flow.tools.sub_agent import create_sub_agent_tool + +__all__ = [ + "create_all_tools", + "create_coding_tools", + "create_core_tools", + "create_execution_tools", + "create_memory_tool", + "create_sub_agent_tool", + "get_tool_schema", + "tool", +] + + +def tool( + name: str | None = None, + description: str | None = None, +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Decorator to mark a function as an agent tool. + + This decorator adds metadata to functions that allows harnesses + to discover and use them as agent tools. + + Args: + name: Tool name (defaults to function name) + description: Tool description (defaults to docstring) + + Returns: + Decorated function with tool metadata + + Example: + @tool(name="read_file", description="Read file contents") + async def read_file(path: str) -> str: + ... + """ + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return func(*args, **kwargs) + + # Store tool metadata + wrapper._tool_name = name or func.__name__ # type: ignore[attr-defined] + wrapper._tool_description = description or func.__doc__ or "" # type: ignore[attr-defined] + wrapper._is_tool = True # type: ignore[attr-defined] + + return wrapper + + return decorator + + +def get_tool_schema(func: Callable[..., Any]) -> dict[str, Any]: + """Extract JSON schema from a tool function. + + Uses type hints and Annotated metadata to build the schema. + + Args: + func: Tool function to extract schema from + + Returns: + JSON schema dict for the tool's parameters + """ + hints = get_type_hints(func, include_extras=True) + sig = inspect.signature(func) + + properties: dict[str, Any] = {} + required: list[str] = [] + + for param_name, param in sig.parameters.items(): + if param_name in ("self", "cls"): + continue + + param_schema: dict[str, Any] = {} + hint = hints.get(param_name, Any) + + # Handle Annotated types + origin = getattr(hint, "__origin__", None) + if origin is not None: + # Check if it's Annotated + if hasattr(hint, "__metadata__"): + # Extract description from Annotated metadata + for meta in hint.__metadata__: + if isinstance(meta, str): + param_schema["description"] = meta + break + # Get the actual type + hint = hint.__args__[0] + origin = getattr(hint, "__origin__", None) + + # Map Python types to JSON schema types + if hint is str: + param_schema["type"] = "string" + elif hint is int: + param_schema["type"] = "integer" + elif hint is float: + param_schema["type"] = "number" + elif hint is bool: + param_schema["type"] = "boolean" + elif origin is list: + param_schema["type"] = "array" + elif origin is dict: + param_schema["type"] = "object" + else: + param_schema["type"] = "string" # Default fallback + + properties[param_name] = param_schema + + # Check if parameter is required (no default value) + if param.default is inspect.Parameter.empty: + required.append(param_name) + + return { + "type": "object", + "properties": properties, + "required": required, + } + + +def create_all_tools( + workspace: Path, + memory_path: Path, + bash_timeout: int = 120, + *, + enable_memory_tool: bool = True, + enable_sub_agent: bool = False, + sub_agent_model: str = "gpt-4o-mini", +) -> Sequence[Callable[..., Any]]: + """Create all standard tools for the Flow agent. + + Args: + workspace: Root directory for file operations + memory_path: Directory for persistent memory + bash_timeout: Timeout for bash commands in seconds + enable_memory_tool: Whether to include the memory tool + enable_sub_agent: Whether to include the sub-agent research tool + sub_agent_model: Model to use for sub-agent (default: gpt-4o-mini) + + Returns: + List of all tool functions + """ + tools: list[Callable[..., Any]] = [] + + # Core tools always included + tools.extend(create_coding_tools(workspace)) + tools.extend(create_execution_tools(workspace, memory_path, bash_timeout)) + tools.extend(create_core_tools()) + + # Optional: Agent-managed memory tool + if enable_memory_tool: + tools.append(create_memory_tool(memory_path)) + + # Optional: Sub-agent for isolated research + if enable_sub_agent: + tools.append(create_sub_agent_tool(workspace, model=sub_agent_model)) + + return tools diff --git a/src/flow/tools/coding.py b/src/flow/tools/coding.py new file mode 100644 index 0000000000000000000000000000000000000000..1186f0a69a67704319859980363647303cc535f0 --- /dev/null +++ b/src/flow/tools/coding.py @@ -0,0 +1,391 @@ +"""Coding tools for file operations and code search. + +These tools enable agents to read/write files, list directories, +and search for patterns in code. + +The agent can read and write to any path the user has access to. +The workspace serves as the default working directory for relative paths. +""" + +import re +from collections.abc import Callable, Coroutine, Sequence +from pathlib import Path +from typing import Annotated, Any + + +def create_read_file_tool(workspace: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a read_file tool that can read from any path. + + Args: + workspace: Default directory for relative paths (not a restriction) + """ + + async def read_file( + file_path: Annotated[str, "Path to the file (absolute or relative to workspace)"], + max_lines: Annotated[int, "Maximum lines to return (default: 500)"] = 500, + ) -> str: + """Read the contents of a file. Can read from any path on the system.""" + try: + # Support both absolute and relative paths + path = Path(file_path) + if path.is_absolute(): + full_path = path.resolve() + else: + full_path = (workspace / file_path).resolve() + + if not full_path.exists(): + return f"Error: File not found: {file_path}" + + if not full_path.is_file(): + return f"Error: Not a file: {file_path}" + + content = full_path.read_text(encoding="utf-8") + lines = content.splitlines() + + # Apply line limit + total_lines = len(lines) + if len(lines) > max_lines: + lines = lines[:max_lines] + truncated_msg = f"\n... (truncated, showing first {max_lines} of {total_lines} lines)" + else: + truncated_msg = "" + + # Format with line numbers + numbered_lines = [f"{i + 1:5d}: {line}" for i, line in enumerate(lines)] + result = "\n".join(numbered_lines) + truncated_msg + + return f"File: {full_path} ({total_lines} lines)\n{'=' * 40}\n{result}" + + except UnicodeDecodeError: + return f"Error: Cannot read file (binary or non-UTF-8): {file_path}" + except PermissionError: + return f"Error: Permission denied: {file_path}" + except Exception as e: + return f"Error reading file: {e}" + + # Add tool metadata + read_file._tool_name = "read_file" # type: ignore[attr-defined] + read_file._tool_description = ( # type: ignore[attr-defined] + "Read the contents of a file. Accepts absolute paths (e.g., /path/to/file) " + "or relative paths (relative to workspace). Returns content with line numbers." + ) + read_file._is_tool = True # type: ignore[attr-defined] + + return read_file + + +def create_write_file_tool(workspace: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a write_file tool. + + Args: + workspace: Default directory for relative paths + """ + + async def write_file( + file_path: Annotated[str, "Path to the file (absolute or relative to workspace)"], + content: Annotated[str | None, "Full content to write (for complete file write)"] = None, + old_str: Annotated[str | None, "Text to replace (for str_replace operation)"] = None, + new_str: Annotated[str | None, "Replacement text (for str_replace operation)"] = None, + insert_line: Annotated[int | None, "Line number to insert at (1-indexed)"] = None, + insert_content: Annotated[str | None, "Content to insert at line"] = None, + ) -> str: + """Write or edit file content. + + Supports: (1) full file write with 'content', + (2) str_replace to replace specific text, + (3) insert_at_line to add content at a specific line. + Creates parent directories if needed. + """ + try: + # Support both absolute and relative paths + path = Path(file_path) + if path.is_absolute(): + full_path = path.resolve() + else: + full_path = (workspace / file_path).resolve() + + # Create parent directories + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Operation 1: Full file write + if content is not None: + full_path.write_text(content, encoding="utf-8") + return f"Successfully wrote {len(content)} characters to {file_path}" + + # Operation 2: str_replace + if old_str is not None and new_str is not None: + if not full_path.exists(): + return f"Error: File not found for str_replace: {file_path}" + + current_content = full_path.read_text(encoding="utf-8") + + if old_str not in current_content: + # Show a snippet of the file to help debug + if len(current_content) > 500: + snippet = current_content[:500] + "..." + else: + snippet = current_content + return ( + f"Error: String to replace not found in file.\n" + f"Searching for: '{old_str[:100]}...'\n" + f"File content preview:\n{snippet}" + ) + + # Replace first occurrence only + new_content = current_content.replace(old_str, new_str, 1) + full_path.write_text(new_content, encoding="utf-8") + return f"Successfully replaced text in {file_path}" + + # Operation 3: insert_at_line + if insert_line is not None and insert_content is not None: + if full_path.exists(): + current_content = full_path.read_text(encoding="utf-8") + lines = current_content.splitlines(keepends=True) + else: + lines = [] + + # Ensure insert_content ends with newline + if not insert_content.endswith("\n"): + insert_content += "\n" + + # Insert at specified line (1-indexed) + insert_index = insert_line - 1 + if insert_index < 0: + return f"Error: Invalid line number: {insert_line}. Must be >= 1." + + # Allow inserting at end + if insert_index > len(lines): + insert_index = len(lines) + + lines.insert(insert_index, insert_content) + new_content = "".join(lines) + full_path.write_text(new_content, encoding="utf-8") + return f"Successfully inserted content at line {insert_line} in {file_path}" + + return "Error: Must provide either 'content', 'old_str' + 'new_str', or 'insert_line' + 'insert_content'" + + except Exception as e: + return f"Error writing file: {e}" + + # Add tool metadata + write_file._tool_name = "write_file" # type: ignore[attr-defined] + write_file._tool_description = ( # type: ignore[attr-defined] + "Write or edit file content. Accepts absolute paths or relative paths (relative to workspace). " + "Supports: (1) full file write with 'content', (2) str_replace to replace specific text, " + "(3) insert_at_line to add content at a specific line. Creates parent directories if needed." + ) + write_file._is_tool = True # type: ignore[attr-defined] + + return write_file + + +def create_list_directory_tool(workspace: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a list_directory tool that can list any directory. + + Args: + workspace: Default directory for relative paths (not a restriction) + """ + + async def list_directory( + directory_path: Annotated[str, "Path to directory (absolute or relative to workspace, default: '.')"] = ".", + recursive: Annotated[bool, "List subdirectories recursively (default: false)"] = False, + max_entries: Annotated[int, "Maximum entries to return (default: 200)"] = 200, + ) -> str: + """List files and directories at a given path. Can list any directory on the system.""" + try: + # Support both absolute and relative paths + path = Path(directory_path) + if path.is_absolute(): + full_path = path.resolve() + else: + full_path = (workspace / directory_path).resolve() + + if not full_path.exists(): + return f"Error: Directory not found: {directory_path}" + + if not full_path.is_dir(): + return f"Error: Not a directory: {directory_path}" + + entries: list[tuple[str, str, int]] = [] + + if recursive: + for item in full_path.rglob("*"): + if len(entries) >= max_entries: + break + # Skip common non-essential directories + skip_dirs = ["node_modules", "__pycache__", ".git", "venv", ".venv"] + if any(part in item.parts for part in skip_dirs): + continue + rel_path = item.relative_to(full_path) + item_type = "file" if item.is_file() else "dir" + size = item.stat().st_size if item.is_file() else 0 + entries.append((str(rel_path), item_type, size)) + else: + for item in full_path.iterdir(): + if len(entries) >= max_entries: + break + item_type = "file" if item.is_file() else "dir" + size = item.stat().st_size if item.is_file() else 0 + entries.append((item.name, item_type, size)) + + # Sort: directories first, then by name + entries.sort(key=lambda x: (x[1] != "dir", x[0])) + + # Format output + result_lines = [f"Directory: {directory_path} ({len(entries)} entries)"] + result_lines.append("=" * 50) + + for name, item_type, size in entries: + if item_type == "dir": + result_lines.append(f" [DIR] {name}/") + else: + size_str = f"{size:,} bytes" if size < 10000 else f"{size / 1024:.1f} KB" + result_lines.append(f" [FILE] {name} ({size_str})") + + if len(entries) >= max_entries: + result_lines.append(f"\n... (truncated at {max_entries} entries)") + + return "\n".join(result_lines) + + except Exception as e: + return f"Error listing directory: {e}" + + # Add tool metadata + list_directory._tool_name = "list_directory" # type: ignore[attr-defined] + list_directory._tool_description = ( # type: ignore[attr-defined] + "List files and directories at a given path. Accepts absolute paths (e.g., /path/to/dir) " + "or relative paths (relative to workspace). Returns names, types, and sizes." + ) + list_directory._is_tool = True # type: ignore[attr-defined] + + return list_directory + + +def create_grep_search_tool(workspace: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a grep_search tool that can search any directory. + + Args: + workspace: Default directory for relative paths (not a restriction) + """ + + async def grep_search( + pattern: Annotated[str, "Pattern to search for (regex supported)"], + path: Annotated[str, "Path to search in (absolute or relative to workspace, default: '.')"] = ".", + file_pattern: Annotated[str | None, "File pattern to filter (e.g., '*.py', '*.js')"] = None, + case_sensitive: Annotated[bool, "Case sensitive search (default: true)"] = True, + max_matches: Annotated[int, "Maximum matches to return (default: 50)"] = 50, + ) -> str: + """Search for text patterns in files. Can search any path on the system.""" + try: + # Support both absolute and relative paths + search_path = Path(path) + if search_path.is_absolute(): + full_path = search_path.resolve() + else: + full_path = (workspace / path).resolve() + + if not full_path.exists(): + return f"Error: Path not found: {path}" + + # Compile regex + flags = 0 if case_sensitive else re.IGNORECASE + try: + regex = re.compile(pattern, flags) + except re.error as e: + return f"Error: Invalid regex pattern: {e}" + + matches: list[dict[str, Any]] = [] + + # Get files to search + if full_path.is_file(): + files = [full_path] + else: + if file_pattern: + files = list(full_path.rglob(file_pattern)) + else: + files = [f for f in full_path.rglob("*") if f.is_file()] + + # Search each file + for file_path_item in files: + if len(matches) >= max_matches: + break + + # Skip common non-essential directories and binary files + skip_dirs = ["node_modules", "__pycache__", ".git", "venv", ".venv"] + if any(part in file_path_item.parts for part in skip_dirs): + continue + + try: + # Skip large files (> 1MB) + if file_path_item.stat().st_size > 1_000_000: + continue + + file_content = file_path_item.read_text(encoding="utf-8", errors="ignore") + lines = file_content.splitlines() + + for line_num, line in enumerate(lines, 1): + if len(matches) >= max_matches: + break + if regex.search(line): + # Compute relative path from search root + try: + rel_path = file_path_item.relative_to(full_path) + except ValueError: + # If file is the search path itself, use filename + rel_path = file_path_item.name + matches.append({ + "file": str(rel_path), + "line": line_num, + "text": line.strip()[:200], + }) + except (UnicodeDecodeError, PermissionError): + continue + + # Format output + if not matches: + return f"No matches found for pattern '{pattern}' in {path}" + + result_lines = [f"Found {len(matches)} match(es) for '{pattern}'"] + result_lines.append("=" * 50) + + for match in matches: + result_lines.append(f"{match['file']}:{match['line']}: {match['text']}") + + if len(matches) >= max_matches: + result_lines.append(f"\n... (truncated at {max_matches} matches)") + + return "\n".join(result_lines) + + except Exception as e: + return f"Error searching: {e}" + + # Add tool metadata + grep_search._tool_name = "grep_search" # type: ignore[attr-defined] + grep_search._tool_description = ( # type: ignore[attr-defined] + "Search for text patterns in files. Accepts absolute paths (e.g., /path/to/dir) " + "or relative paths (relative to workspace). Supports regex patterns and file filtering." + ) + grep_search._is_tool = True # type: ignore[attr-defined] + + return grep_search + + +def create_coding_tools(workspace: Path) -> Sequence[Callable[..., Coroutine[Any, Any, str]]]: + """Create all coding tools bound to a workspace. + + Args: + workspace: Root directory for file operations + + Returns: + List of coding tool functions + """ + workspace = Path(workspace).resolve() + + return [ + create_read_file_tool(workspace), + create_write_file_tool(workspace), + create_list_directory_tool(workspace), + create_grep_search_tool(workspace), + ] + + diff --git a/src/flow/tools/core.py b/src/flow/tools/core.py new file mode 100644 index 0000000000000000000000000000000000000000..15fc31452ca3ad90ee82be2056bda0bdbdfdbeed --- /dev/null +++ b/src/flow/tools/core.py @@ -0,0 +1,100 @@ +"""Core metacognitive tools for agent reasoning and task management. + +These tools enable agents to think explicitly, track task status, +and make structured decisions during complex software engineering tasks. +""" + +from collections.abc import Callable, Coroutine, Sequence +from typing import Annotated, Any, Literal + + +async def think( + thought: Annotated[ + str, + ( + "Your detailed reasoning about the current situation. " + "Include: what you've learned, options you're considering, " + "potential risks, and your planned approach." + ), + ], +) -> str: + """Use this tool to pause and think through a complex problem. + + Helpful when: (1) analyzing tool results, (2) planning multi-step approaches, + (3) making design decisions, (4) debugging issues, (5) avoiding mistakes. + Your reasoning is recorded and helps structure your approach. + """ + # The value is in giving the LLM dedicated space to reason + summary = thought[:300] + "..." if len(thought) > 300 else thought + return f"Thought recorded: {summary}" + + +async def task_done( + status: Annotated[ + Literal["complete", "incomplete"], + "'complete' if task finished successfully, 'incomplete' if blocked or needs input", + ], + summary: Annotated[ + str, + ( + "Summary of what was accomplished. " + "If complete: what was done and how to use/test it. " + "If incomplete: what's blocking and what's needed." + ), + ], + files_created: Annotated[ + list[str] | None, + "List of files created or modified (if any)", + ] = None, + next_steps: Annotated[ + list[str] | None, + "Suggested next steps for the user (if any)", + ] = None, +) -> str: + """Call this when you have completed the user's task. + + Provide a summary of what was accomplished and any relevant details. + Use 'complete' if all requirements are satisfied, + 'incomplete' if blocked or need more information. + """ + result_lines = [ + f"Task Status: {status.upper()}", + "", + "Summary:", + summary, + ] + + if files_created: + result_lines.extend([ + "", + "Files Created/Modified:", + *[f" - {f}" for f in files_created], + ]) + + if next_steps: + result_lines.extend([ + "", + "Suggested Next Steps:", + *[f" - {step}" for step in next_steps], + ]) + + return "\n".join(result_lines) + + +# Add tool metadata +think._tool_name = "think" # type: ignore[attr-defined] +think._tool_description = think.__doc__ or "" # type: ignore[attr-defined] +think._is_tool = True # type: ignore[attr-defined] + +task_done._tool_name = "task_done" # type: ignore[attr-defined] +task_done._tool_description = task_done.__doc__ or "" # type: ignore[attr-defined] +task_done._is_tool = True # type: ignore[attr-defined] + + +def create_core_tools() -> Sequence[Callable[..., Coroutine[Any, Any, str]]]: + """Create all core metacognitive tools. + + Returns: + List of core tool functions + """ + return [think, task_done] diff --git a/src/flow/tools/execution.py b/src/flow/tools/execution.py new file mode 100644 index 0000000000000000000000000000000000000000..b1395206989c4723dd55df12c8c7f467675816d2 --- /dev/null +++ b/src/flow/tools/execution.py @@ -0,0 +1,479 @@ +"""Execution tools for running commands and code. + +These tools enable agents to execute bash commands and Python code +with safety controls (timeouts, output limits), and manage background processes. +""" + +import asyncio +import os +import re +import signal +import sys +from collections.abc import Callable, Coroutine, Sequence +from datetime import datetime +from io import StringIO +from pathlib import Path +from typing import Annotated, Any, Literal + + +def _get_process_registry_path(memory_path: Path) -> Path: + """Get the path to the process registry file in memory.""" + return memory_path / "processes.md" + + +def _ensure_process_registry(memory_path: Path) -> Path: + """Ensure the process registry file exists and return its path.""" + registry_path = _get_process_registry_path(memory_path) + registry_path.parent.mkdir(parents=True, exist_ok=True) + + if not registry_path.exists(): + registry_path.write_text( + "# Background Processes\n\n" + "This file tracks background processes started by the Flow agent.\n" + "You can view this file with `memory(command='view', path='/memory/processes.md')`\n\n" + "## Running\n\n" + "## Stopped\n\n" + ) + return registry_path + + +def _add_process_to_registry( + memory_path: Path, + pid: int, + command: str, + workspace: str, + log_file: str, + port: int | None = None, +) -> None: + """Add a process to the registry using checklist format.""" + registry_path = _ensure_process_registry(memory_path) + content = registry_path.read_text() + + # Extract port from command if not provided + if port is None: + port_match = re.search(r"(?:--port|-p)\s+(\d+)", command) + if port_match: + port = int(port_match.group(1)) + elif ":8000" in command or "8000" in command: + port = 8000 + elif ":3000" in command or "3000" in command: + port = 3000 + + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + port_str = f"Port: {port}" if port else "Port: -" + cmd_short = command[:60] + "..." if len(command) > 60 else command + workspace_short = workspace.split("/")[-1] if "/" in workspace else workspace + + # Create checklist entry + entry = f"- [ ] **PID {pid}** | `{cmd_short}` | {timestamp} | {port_str} | {workspace_short}\n" + + # Add under "## Running" section + if "## Running" in content: + content = content.replace("## Running\n\n", f"## Running\n\n{entry}") + else: + # Add Running section if missing + content += f"\n## Running\n\n{entry}" + + registry_path.write_text(content) + + +def _mark_process_stopped(memory_path: Path, pid: int, reason: str = "killed") -> None: + """Mark a process as stopped in the registry (check the box and move to Stopped).""" + registry_path = _get_process_registry_path(memory_path) + if not registry_path.exists(): + return + + content = registry_path.read_text() + lines = content.split("\n") + new_lines: list[str] = [] + stopped_entry: str | None = None + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + + for line in lines: + if f"**PID {pid}**" in line and "- [ ]" in line: + # Found the running process - mark it as checked and prepare for Stopped section + stopped_entry = line.replace("- [ ]", "- [x]") + f" | {reason} @ {timestamp}" + # Don't add to new_lines yet (will move to Stopped section) + else: + new_lines.append(line) + + # Add stopped entry to Stopped section + if stopped_entry: + content = "\n".join(new_lines) + if "## Stopped" in content: + content = content.replace("## Stopped\n\n", f"## Stopped\n\n{stopped_entry}\n") + else: + content += f"\n## Stopped\n\n{stopped_entry}\n" + registry_path.write_text(content) + + +def _is_process_running(pid: int) -> bool: + """Check if a process is still running.""" + try: + os.kill(pid, 0) + return True + except (OSError, ProcessLookupError): + return False + + +def _get_running_pids_from_registry(memory_path: Path) -> list[tuple[int, str]]: + """Get list of (pid, line) for processes marked as running in registry.""" + registry_path = _get_process_registry_path(memory_path) + if not registry_path.exists(): + return [] + + content = registry_path.read_text() + running: list[tuple[int, str]] = [] + + for line in content.split("\n"): + if "- [ ]" in line and "**PID" in line: + # Extract PID from format: **PID 12345** + match = re.search(r"\*\*PID (\d+)\*\*", line) + if match: + pid = int(match.group(1)) + running.append((pid, line)) + + return running + + +def create_bash_execute_tool( + workspace: Path, memory_path: Path, default_timeout: int = 120 +) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a bash_execute tool bound to a specific workspace.""" + + async def bash_execute( + command: Annotated[str, "Bash command to execute"], + timeout: Annotated[int, f"Command timeout in seconds (default: {default_timeout})"] = default_timeout, + background: Annotated[ + bool, "Run in background and return immediately with PID. Use for servers/long-running processes." + ] = False, + ) -> str: + """Execute bash commands in the workspace. + + Returns stdout, stderr, and return code. + Use for running tests, git commands, package managers, builds, etc. + IMPORTANT: Each call runs in a fresh shell from workspace root - + use 'cd dir && command' for commands in subdirectories. + For long-running processes (servers), use background=True to avoid timeout. + """ + try: + if background: + # Run in background using nohup and capture PID + # Redirect output to a log file + log_file = workspace / ".background_logs" / f"bg_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + + bg_command = f"nohup {command} > {log_file} 2>&1 & echo $!" + + proc = await asyncio.create_subprocess_shell( + bg_command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=str(workspace), + ) + + stdout, _ = await proc.communicate() + pid_str = stdout.decode().strip() + + try: + pid = int(pid_str) + # Register the process in memory + _add_process_to_registry( + memory_path=memory_path, + pid=pid, + command=command, + workspace=str(workspace), + log_file=str(log_file), + ) + + return ( + f"Background process started successfully.\n" + f"PID: {pid}\n" + f"Command: {command}\n" + f"Log file: {log_file}\n" + f"\nProcess registered in /memory/processes.md\n" + f"Use check_processes(action='list') to see all background processes.\n" + f"Use check_processes(action='kill', pid={pid}) to stop this process." + ) + except ValueError: + return f"Error: Could not get PID. Output: {pid_str}" + + # Regular (blocking) execution + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=str(workspace), + ) + + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + return ( + f"Error: Command timed out after {timeout} seconds.\n" + f"Command: {command}\n\n" + f"TIP: If this is a long-running process (like a server), " + f"use background=True to run it in the background." + ) + + stdout_str = stdout.decode("utf-8", errors="replace") + stderr_str = stderr.decode("utf-8", errors="replace") + return_code = proc.returncode + + # Format output + result_parts = [f"Command: {command}"] + result_parts.append(f"Return code: {return_code}") + result_parts.append("=" * 50) + + if stdout_str.strip(): + # Truncate very long output + if len(stdout_str) > 15000: + stdout_str = stdout_str[:15000] + "\n... (stdout truncated)" + result_parts.append("STDOUT:") + result_parts.append(stdout_str) + + if stderr_str.strip(): + if len(stderr_str) > 5000: + stderr_str = stderr_str[:5000] + "\n... (stderr truncated)" + result_parts.append("STDERR:") + result_parts.append(stderr_str) + + if not stdout_str.strip() and not stderr_str.strip(): + result_parts.append("(no output)") + + return "\n".join(result_parts) + + except Exception as e: + return f"Error executing command: {e}" + + # Add tool metadata + bash_execute._tool_name = "bash_execute" # type: ignore[attr-defined] + bash_execute._tool_description = ( # type: ignore[attr-defined] + "Execute bash commands in the workspace. " + "Returns stdout, stderr, and return code. " + "Use for running tests, git commands, package managers, builds, etc." + ) + bash_execute._is_tool = True # type: ignore[attr-defined] + + return bash_execute + + +def create_check_processes_tool( + workspace: Path, memory_path: Path +) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a tool to check and manage background processes.""" + + async def check_processes( + action: Annotated[ + Literal["list", "kill", "cleanup"], + "'list' to see processes, 'kill' to stop one by PID, 'cleanup' to kill all", + ], + pid: Annotated[int | None, "PID to kill (required for 'kill' action)"] = None, + ) -> str: + """Check and manage background processes. + + Use 'list' to see all background processes (also viewable at /memory/processes.md), + 'kill' to stop a specific process by PID, + 'cleanup' to kill all background processes from this workspace. + """ + _ensure_process_registry(memory_path) + registry_path = _get_process_registry_path(memory_path) + + if action == "list": + # Read the registry and update status of running processes + running_pids = _get_running_pids_from_registry(memory_path) + active_count = 0 + dead_pids: list[int] = [] + + for proc_pid, _ in running_pids: + if _is_process_running(proc_pid): + active_count += 1 + else: + dead_pids.append(proc_pid) + + # Mark dead processes as stopped + for dead_pid in dead_pids: + _mark_process_stopped(memory_path, dead_pid, reason="exited") + + # Return the updated registry + content = registry_path.read_text() + return ( + f"Active background processes: {active_count}\n" + f"(View full registry at /memory/processes.md)\n\n" + f"{content}" + ) + + if action == "kill": + if pid is None: + return "Error: 'pid' is required for 'kill' action." + + try: + os.kill(pid, signal.SIGTERM) + await asyncio.sleep(0.5) # Give it time to terminate + + # Check if it's really dead, if not SIGKILL + if _is_process_running(pid): + os.kill(pid, signal.SIGKILL) + await asyncio.sleep(0.2) + + _mark_process_stopped(memory_path, pid, reason="killed") + + if _is_process_running(pid): + return f"Warning: Process {pid} may still be running after kill attempt." + return f"Successfully killed process {pid}. Updated /memory/processes.md" + + except ProcessLookupError: + _mark_process_stopped(memory_path, pid, reason="not found") + return f"Process {pid} was not running (already terminated). Updated /memory/processes.md" + except PermissionError: + return f"Error: Permission denied to kill process {pid}." + except Exception as e: + return f"Error killing process {pid}: {e}" + + if action == "cleanup": + # Kill all processes from this workspace + running_pids = _get_running_pids_from_registry(memory_path) + workspace_str = str(workspace) + killed: list[int] = [] + failed: list[tuple[int, str]] = [] + + for proc_pid, line in running_pids: + # Check if this process is from our workspace + workspace_short = workspace_str.split("/")[-1] + if workspace_short in line or workspace_str in line: + try: + os.kill(proc_pid, signal.SIGTERM) + await asyncio.sleep(0.2) + if _is_process_running(proc_pid): + os.kill(proc_pid, signal.SIGKILL) + _mark_process_stopped(memory_path, proc_pid, reason="cleanup") + killed.append(proc_pid) + except (ProcessLookupError, PermissionError) as e: + _mark_process_stopped(memory_path, proc_pid, reason=f"cleanup failed: {e}") + failed.append((proc_pid, str(e))) + + result = "Cleanup complete. Updated /memory/processes.md\n" + if killed: + result += f"Killed processes: {killed}\n" + if failed: + result += f"Failed to kill: {failed}\n" + if not killed and not failed: + result += "No active processes found for this workspace." + + return result + + return f"Unknown action: {action}" + + # Add tool metadata + check_processes._tool_name = "check_processes" # type: ignore[attr-defined] + check_processes._tool_description = ( # type: ignore[attr-defined] + "Check and manage background processes. " + "Use 'list' to see all background processes, " + "'kill' to stop a specific process by PID, " + "'cleanup' to kill all background processes from this workspace." + ) + check_processes._is_tool = True # type: ignore[attr-defined] + + return check_processes + + +def create_python_repl_tool(workspace: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a python_repl tool bound to a specific workspace.""" + + async def python_repl( + code: Annotated[str, "Python code to execute"], + ) -> str: + """Execute Python code in an isolated namespace. + + Returns the output (stdout) or any errors. + Use for testing code snippets, calculations, data manipulation, or quick validation. + The WORKSPACE variable is available with the workspace path. + """ + old_stdout = sys.stdout + old_stderr = sys.stderr + + try: + # Capture stdout and stderr + redirected_output = StringIO() + redirected_error = StringIO() + sys.stdout = redirected_output + sys.stderr = redirected_error + + # Create isolated namespace with builtins + namespace: dict[str, Any] = { + "__builtins__": __builtins__, + "__name__": "__main__", + "WORKSPACE": workspace, + } + + try: + # Try to compile and exec + compiled = compile(code, "", "exec") + exec(compiled, namespace) # noqa: S102 + + output = redirected_output.getvalue() + error = redirected_error.getvalue() + + result_parts = ["Python REPL Output"] + result_parts.append("=" * 50) + + if output.strip(): + if len(output) > 15000: + output = output[:15000] + "\n... (output truncated)" + result_parts.append(output) + + if error.strip(): + result_parts.append("STDERR:") + result_parts.append(error) + + if not output.strip() and not error.strip(): + result_parts.append("(code executed successfully, no output)") + + return "\n".join(result_parts) + + except SyntaxError as e: + return f"SyntaxError: {e}" + except Exception as e: + return f"Error: {type(e).__name__}: {e}" + + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + # Add tool metadata + python_repl._tool_name = "python_repl" # type: ignore[attr-defined] + python_repl._tool_description = ( # type: ignore[attr-defined] + "Execute Python code in an isolated namespace. " + "Returns the output (stdout) or any errors. " + "Use for testing code snippets, calculations, data manipulation, or quick validation." + ) + python_repl._is_tool = True # type: ignore[attr-defined] + + return python_repl + + +def create_execution_tools( + workspace: Path, + memory_path: Path, + bash_timeout: int = 120, +) -> Sequence[Callable[..., Coroutine[Any, Any, str]]]: + """Create all execution tools bound to a workspace. + + Args: + workspace: Root directory for command execution + memory_path: Path to memory directory for process registry + bash_timeout: Default timeout for bash commands in seconds + + Returns: + List of execution tool functions + """ + workspace = Path(workspace).resolve() + memory_path = Path(memory_path).resolve() + + return [ + create_bash_execute_tool(workspace, memory_path, bash_timeout), + create_check_processes_tool(workspace, memory_path), + create_python_repl_tool(workspace), + ] diff --git a/src/flow/tools/memory.py b/src/flow/tools/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..1a79a6d9d1feb612d4b34de45f5950f8509921ea --- /dev/null +++ b/src/flow/tools/memory.py @@ -0,0 +1,260 @@ +"""Memory tool for persistent storage across sessions. + +Provides file-based memory storage allowing agents to store and retrieve +information, patterns, and decisions across conversations. +""" + +from collections.abc import Callable, Coroutine +from pathlib import Path +from typing import Annotated, Any, Literal + + +class MemoryBackend: + """File-based memory storage backend with security controls.""" + + def __init__(self, base_path: Path) -> None: + """Initialize memory backend.""" + self.base_path = Path(base_path).resolve() + self.base_path.mkdir(parents=True, exist_ok=True) + + def _validate_path(self, path: str) -> Path: + """Validate and resolve a memory path.""" + # Normalize path (remove /memory prefix if present) + if path.startswith("/memory"): + path = path[len("/memory") :] + path = path.lstrip("/") + + # Handle empty path + if not path: + return self.base_path + + # Resolve to absolute path + full_path = (self.base_path / path).resolve() + + # Security: Ensure path is within base_path + try: + full_path.relative_to(self.base_path) + except ValueError as err: + raise ValueError(f"Access denied: path '{path}' is outside memory directory") from err + + return full_path + + def view(self, path: str, view_range: list[int] | None = None) -> str: + """View directory contents or file contents.""" + full_path = self._validate_path(path) + + if not full_path.exists(): + return f"Path not found: {path}\nUse 'create' to create new files." + + # Directory listing + if full_path.is_dir(): + contents = [f"Directory: {path or '/memory'}"] + items = sorted(full_path.iterdir(), key=lambda x: (x.is_file(), x.name)) + + if not items: + contents.append("(empty directory)") + else: + for item in items: + suffix = "/" if item.is_dir() else "" + contents.append(f" - {item.name}{suffix}") + + return "\n".join(contents) + + # File contents + if full_path.is_file(): + content = full_path.read_text(encoding="utf-8") + lines = content.splitlines() + + if view_range: + start, end = view_range + start = max(1, start) + end = min(len(lines), end) + lines = lines[start - 1 : end] + numbered_lines = [f"{i + start:5d}: {line}" for i, line in enumerate(lines)] + else: + numbered_lines = [f"{i + 1:5d}: {line}" for i, line in enumerate(lines)] + + return "\n".join(numbered_lines) if numbered_lines else "(empty file)" + + return f"Unknown path type: {path}" + + def create(self, path: str, file_text: str) -> str: + """Create or overwrite a file.""" + full_path = self._validate_path(path) + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(file_text, encoding="utf-8") + return f"File created successfully at {path}" + + def str_replace(self, path: str, old_str: str, new_str: str) -> str: + """Replace text in a file.""" + full_path = self._validate_path(path) + + if not full_path.is_file(): + raise FileNotFoundError(f"File not found: {path}") + + content = full_path.read_text(encoding="utf-8") + + if old_str not in content: + raise ValueError(f"Text not found in file: '{old_str[:50]}...'") + + new_content = content.replace(old_str, new_str, 1) + full_path.write_text(new_content, encoding="utf-8") + return f"File {path} has been edited successfully" + + def append(self, path: str, text: str) -> str: + """Append text to end of file.""" + full_path = self._validate_path(path) + + if not full_path.exists(): + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text("", encoding="utf-8") + + # Ensure text starts with newline if file isn't empty + if full_path.stat().st_size > 0: + existing = full_path.read_text(encoding="utf-8") + if existing and not existing.endswith("\n"): + text = "\n" + text + + # Ensure text ends with newline + if not text.endswith("\n"): + text += "\n" + + with full_path.open("a", encoding="utf-8") as f: + f.write(text) + + return f"Text appended to {path}" + + def search(self, query: str, path: str = "") -> str: + """Search for text across memory files.""" + full_path = self._validate_path(path) + + if not full_path.exists(): + return f"Path not found: {path or '/memory'}" + + if not full_path.is_dir(): + # Search single file + files = [full_path] + else: + files = list(full_path.rglob("*")) + + matches: list[dict[str, Any]] = [] + query_lower = query.lower() + + for file_path in files: + if not file_path.is_file(): + continue + try: + content = file_path.read_text(encoding="utf-8") + lines = content.splitlines() + + for line_num, line in enumerate(lines, 1): + if query_lower in line.lower(): + rel_path = file_path.relative_to(self.base_path) + matches.append({ + "file": str(rel_path), + "line": line_num, + "content": line.strip()[:100], + }) + except (UnicodeDecodeError, PermissionError): + continue + + if not matches: + return f"No matches found for '{query}' in {path or '/memory'}" + + result_lines = [f"Found {len(matches)} match(es) for '{query}':\n"] + for match in matches[:50]: + result_lines.append(f" {match['file']}:{match['line']} - {match['content']}") + + if len(matches) > 50: + result_lines.append(f"\n... and {len(matches) - 50} more matches") + + return "\n".join(result_lines) + + def delete(self, path: str) -> str: + """Delete a file or empty directory.""" + full_path = self._validate_path(path) + + if not full_path.exists(): + raise FileNotFoundError(f"Path not found: {path}") + + if full_path.is_file(): + full_path.unlink() + return f"File deleted: {path}" + + if full_path.is_dir(): + if any(full_path.iterdir()): + raise ValueError(f"Directory not empty: {path}. Delete contents first.") + full_path.rmdir() + return f"Directory deleted: {path}" + + return f"Unknown path type: {path}" + + +def create_memory_tool(memory_path: Path) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a memory tool bound to a specific memory directory.""" + backend = MemoryBackend(memory_path) + + async def memory( + command: Annotated[ + Literal["view", "create", "str_replace", "append", "search", "delete"], + "Operation to perform", + ], + path: Annotated[str, "Path to file or directory (e.g., '/memory/patterns/cors.md')"] = "/memory", + file_text: Annotated[str | None, "Content to write (for create)"] = None, + old_str: Annotated[str | None, "Text to find (for str_replace)"] = None, + new_str: Annotated[str | None, "Replacement text (for str_replace)"] = None, + append_text: Annotated[str | None, "Text to append (for append)"] = None, + query: Annotated[str | None, "Search query (for search)"] = None, + view_range: Annotated[list[int] | None, "Line range [start, end] (for view)"] = None, + ) -> str: + """Store and retrieve information in persistent memory. + + Memory persists across conversations - use it to remember patterns, + insights, project context, and decisions. + Operations: view (show directory/file), create (new file), + str_replace (edit file), append (add to file), + search (find text), delete (remove file/dir). + Organize by: /memory/patterns/, /memory/projects/, /memory/decisions/ + """ + try: + if command == "view": + return backend.view(path, view_range) + + if command == "create": + if file_text is None: + return "Error: 'file_text' is required for create operation" + return backend.create(path, file_text) + + if command == "str_replace": + if old_str is None or new_str is None: + return "Error: 'old_str' and 'new_str' are required for str_replace" + return backend.str_replace(path, old_str, new_str) + + if command == "append": + if append_text is None: + return "Error: 'append_text' is required for append operation" + return backend.append(path, append_text) + + if command == "search": + if query is None: + return "Error: 'query' is required for search operation" + return backend.search(query, path) + + if command == "delete": + return backend.delete(path) + + return f"Error: Unknown command: {command}" + + except Exception as e: + return f"Memory operation failed: {e}" + + # Add tool metadata + memory._tool_name = "memory" # type: ignore[attr-defined] + memory._tool_description = ( # type: ignore[attr-defined] + "Store and retrieve information in persistent memory. " + "Memory persists across conversations - use it to remember patterns, " + "insights, project context, and decisions." + ) + memory._is_tool = True # type: ignore[attr-defined] + + return memory diff --git a/src/flow/tools/sub_agent.py b/src/flow/tools/sub_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..d59587f32106a21a8dd4cec6484ea4f93aa2a5e3 --- /dev/null +++ b/src/flow/tools/sub_agent.py @@ -0,0 +1,188 @@ +"""Sub-agent tool for isolated research tasks. + +Provides context isolation by delegating complex research tasks to a +separate agent that operates in its own context window. The sub-agent +processes the request and returns only a concise summary, preventing +context pollution in the main agent. + +This implements the "Isolation" strategy for context engineering: +- Coordinator agent stays lean with minimal context +- Sub-agent can use 30K+ tokens internally for research +- Only the distilled result (200-500 tokens) returns to coordinator +""" + +from __future__ import annotations + +import os +from collections.abc import Callable, Coroutine +from pathlib import Path +from typing import Annotated, Any + +# Sub-agent system prompt focused on research and summarization +SUB_AGENT_INSTRUCTIONS = """You are a research assistant that helps with complex information gathering tasks. + +Your role: +1. Thoroughly research the given topic or question +2. Gather relevant information from available tools +3. Synthesize findings into a clear, concise summary +4. Return ONLY the essential information needed by the requesting agent + +Guidelines: +- Be thorough in your research but concise in your response +- Focus on facts and actionable information +- If you can't find information, say so clearly +- Your response will be passed to another agent, so make it self-contained +- Target 200-500 tokens for your final response unless more detail is explicitly requested + +Do NOT: +- Include conversational fluff or preamble +- Repeat the original question back +- Add disclaimers about your limitations +- Include information that wasn't requested +""" + + +def create_sub_agent_tool( + workspace: Path, + model: str = "gpt-4o-mini", + endpoint: str | None = None, + api_key: str | None = None, + api_version: str = "2024-02-15-preview", +) -> Callable[..., Coroutine[Any, Any, str]]: + """Create a sub-agent tool for isolated research tasks. + + The sub-agent runs in its own isolated context, preventing context + pollution in the main agent. This is useful for: + - Complex research that requires many tool calls + - Tasks that generate lots of intermediate content + - Keeping the main agent's context lean and focused + + Args: + workspace: Workspace directory for file operations + model: Model to use for sub-agent (default: gpt-4o-mini for efficiency) + endpoint: Azure OpenAI endpoint (defaults to AZURE_OPENAI_ENDPOINT env var) + api_key: Azure OpenAI API key (defaults to AZURE_OPENAI_API_KEY env var) + api_version: Azure OpenAI API version + + Returns: + An async function that can be used as a tool + """ + # Resolve credentials from environment if not provided + _endpoint = endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT", "") + _api_key = api_key or os.environ.get("AZURE_OPENAI_API_KEY", "") + + # Lazy import to avoid circular dependencies + _sub_agent: Any = None + + async def _ensure_sub_agent() -> Any: + """Lazily create the sub-agent on first use.""" + nonlocal _sub_agent + if _sub_agent is not None: + return _sub_agent + + try: + from agent_framework import ChatAgent + from agent_framework.azure import AzureOpenAIChatClient + except ImportError as e: + raise ImportError( + "Microsoft Agent Framework is required for sub-agent. " + "Install with: pip install agent-framework-core" + ) from e + + # Create a lightweight chat client for the sub-agent + # Uses a smaller/faster model by default for efficiency + client = AzureOpenAIChatClient( + api_key=_api_key, + endpoint=_endpoint, + deployment=model, + api_version=api_version, + ) + + # Create basic tools for the sub-agent + # Keep it minimal - just what's needed for research + from flow.tools.coding import create_coding_tools + from flow.tools.core import create_core_tools + + sub_tools: list[Callable[..., Any]] = [] + sub_tools.extend(create_coding_tools(workspace)) + sub_tools.extend(create_core_tools()) + + # Convert tools to agent_framework format + from agent_framework import ai_function + + converted_tools = [] + for tool_func in sub_tools: + name = getattr(tool_func, "_tool_name", tool_func.__name__) + description = getattr(tool_func, "_tool_description", tool_func.__doc__ or "") + wrapped = ai_function(name=name, description=description)(tool_func) + converted_tools.append(wrapped) + + _sub_agent = ChatAgent( + name="ResearchAssistant", + description="Research assistant for complex information gathering", + instructions=SUB_AGENT_INSTRUCTIONS, + chat_client=client, + tools=converted_tools, + ) + + return _sub_agent + + async def research( + task: Annotated[ + str, + "The research task or question to investigate. Be specific about what information you need.", + ], + context: Annotated[ + str | None, + "Optional context to help the sub-agent understand the broader goal.", + ] = None, + ) -> str: + """Delegate a research task to a sub-agent with isolated context. + + Use this tool when you need to: + - Research a complex topic that may require multiple steps + - Gather information without polluting your main context + - Get a summarized answer to a specific question + + The sub-agent operates in its own context window, so it can + use many tokens internally while only returning a concise summary. + This keeps your main context lean and focused. + + Examples: + - "Find all Python files that import the requests library and summarize their purpose" + - "Research how authentication is implemented in this codebase" + - "Analyze the error handling patterns used across the project" + """ + sub_agent = await _ensure_sub_agent() + + # Build the research prompt + prompt_parts = [f"Research task: {task}"] + if context: + prompt_parts.insert(0, f"Context: {context}") + prompt_parts.append("\nProvide a concise summary of your findings.") + + full_prompt = "\n\n".join(prompt_parts) + + try: + # Run the sub-agent - it operates in isolated context + response = await sub_agent.run(full_prompt) + + # Extract text content from response + if hasattr(response, "content"): + return str(response.content) + return str(response) + + except Exception as e: + return f"Research failed: {e}" + + # Add tool metadata + research._tool_name = "research" # type: ignore[attr-defined] + research._tool_description = ( # type: ignore[attr-defined] + "Delegate a research task to a sub-agent with isolated context. " + "The sub-agent can thoroughly investigate a topic using many tool calls " + "internally, then return only a concise summary. Use this for complex " + "research that would otherwise pollute your main context." + ) + research._is_tool = True # type: ignore[attr-defined] + + return research diff --git a/src/flow/ui/__init__.py b/src/flow/ui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ad36b554811efd51a3cee8965b1fda99d086fa4f --- /dev/null +++ b/src/flow/ui/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Flow UI Backend - FastAPI server.""" diff --git a/src/flow/ui/api/__init__.py b/src/flow/ui/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a074612b673a6d08a931282a0fccd5a786114405 --- /dev/null +++ b/src/flow/ui/api/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft. All rights reserved. +"""API routes package.""" + +from .configs import router as configs_router +from .tasks import router as tasks_router +from .jobs import router as jobs_router +from .runs import router as runs_router + +__all__ = [ + "configs_router", + "tasks_router", + "jobs_router", + "runs_router", +] diff --git a/src/flow/ui/api/configs.py b/src/flow/ui/api/configs.py new file mode 100644 index 0000000000000000000000000000000000000000..0fba7d83f8c5618a7bec4f3e1beb29c79ce6c5d3 --- /dev/null +++ b/src/flow/ui/api/configs.py @@ -0,0 +1,121 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Config API routes.""" + +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select, desc + +from ..database import get_session +from ..models.config import AgentConfig +from ..schemas import ConfigCreate, ConfigUpdate, ConfigResponse + +router = APIRouter(prefix="/configs", tags=["configs"]) + + +def parse_uuid(id_str: str) -> UUID: + """Parse a string to UUID, raising 400 if invalid.""" + try: + return UUID(id_str) + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid UUID: {id_str}") from e + + +@router.get("", response_model=list[ConfigResponse]) +async def list_configs(session: AsyncSession = Depends(get_session)) -> list[AgentConfig]: + """List all agent configurations.""" + result = await session.execute(select(AgentConfig).order_by(desc(AgentConfig.created_at))) + return list(result.scalars().all()) + + +@router.post("", response_model=ConfigResponse, status_code=201) +async def create_config( + data: ConfigCreate, + session: AsyncSession = Depends(get_session), +) -> AgentConfig: + """Create a new agent configuration.""" + config = AgentConfig( + name=data.name, + description=data.description, + config_json=data.to_config_json(), + ) + session.add(config) + await session.commit() + await session.refresh(config) + return config + + +@router.get("/{config_id}", response_model=ConfigResponse) +async def get_config( + config_id: str, + session: AsyncSession = Depends(get_session), +) -> AgentConfig: + """Get a specific agent configuration.""" + uuid_id = parse_uuid(config_id) + result = await session.execute(select(AgentConfig).where(AgentConfig.id == uuid_id)) + config = result.scalar_one_or_none() + if not config: + raise HTTPException(status_code=404, detail="Config not found") + return config + + +@router.put("/{config_id}", response_model=ConfigResponse) +async def update_config( + config_id: str, + data: ConfigUpdate, + session: AsyncSession = Depends(get_session), +) -> AgentConfig: + """Update an agent configuration.""" + uuid_id = parse_uuid(config_id) + result = await session.execute(select(AgentConfig).where(AgentConfig.id == uuid_id)) + config = result.scalar_one_or_none() + if not config: + raise HTTPException(status_code=404, detail="Config not found") + + # Update fields that were provided + update_data = data.model_dump(exclude_unset=True) + + # Handle config_json fields separately + config_fields = [ + "enable_message_compaction", + "enable_memory_tool", + "enable_sub_agent", + "compaction_head_size", + "compaction_tail_size", + "bash_timeout", + ] + + config_json = dict(config.config_json) + for field in config_fields: + if field in update_data: + config_json[field] = update_data.pop(field) + + # Update top-level fields + for key, value in update_data.items(): + setattr(config, key, value) + + config.config_json = config_json + + from datetime import datetime, timezone + config.updated_at = datetime.now(timezone.utc) + + await session.commit() + await session.refresh(config) + return config + + +@router.delete("/{config_id}", status_code=204) +async def delete_config( + config_id: str, + session: AsyncSession = Depends(get_session), +) -> None: + """Delete an agent configuration.""" + uuid_id = parse_uuid(config_id) + result = await session.execute(select(AgentConfig).where(AgentConfig.id == uuid_id)) + config = result.scalar_one_or_none() + if not config: + raise HTTPException(status_code=404, detail="Config not found") + + await session.delete(config) + await session.commit() diff --git a/src/flow/ui/api/jobs.py b/src/flow/ui/api/jobs.py new file mode 100644 index 0000000000000000000000000000000000000000..ac2b4653df2a068c158c4f965f305cf336341e5d --- /dev/null +++ b/src/flow/ui/api/jobs.py @@ -0,0 +1,169 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Job API routes.""" + +import asyncio +from typing import Any, AsyncGenerator +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select, desc + +from ..database import get_session +from ..models.job import OptimizationJob, JobStatus +from ..models.config import AgentConfig +from ..models.task import TaskModel +from ..schemas import JobCreate, JobResponse +from ..services.optimizer_service import OptimizerService + +router = APIRouter(prefix="/jobs", tags=["jobs"]) + +# Store running jobs for cancellation +_running_jobs: dict[str, asyncio.Task[Any]] = {} + + +def parse_uuid(id_str: str) -> UUID: + """Parse a string to UUID, raising 400 if invalid.""" + try: + return UUID(id_str) + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid UUID: {id_str}") from e + + +@router.get("", response_model=list[JobResponse]) +async def list_jobs( + status: JobStatus | None = None, + session: AsyncSession = Depends(get_session), +) -> list[OptimizationJob]: + """List all optimization jobs.""" + query = select(OptimizationJob) + if status: + query = query.where(OptimizationJob.status == status) + query = query.order_by(desc(OptimizationJob.created_at)) + result = await session.execute(query) + return list(result.scalars().all()) + + +@router.post("", response_model=JobResponse, status_code=201) +async def create_job( + data: JobCreate, + session: AsyncSession = Depends(get_session), +) -> OptimizationJob: + """Create a new optimization job.""" + # Validate config_ids exist + for config_id in data.config_ids: + uuid_id = parse_uuid(config_id) + result = await session.execute(select(AgentConfig).where(AgentConfig.id == uuid_id)) + if not result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail=f"Config {config_id} not found") + + # Validate task_ids exist + for task_id in data.task_ids: + uuid_id = parse_uuid(task_id) + result = await session.execute(select(TaskModel).where(TaskModel.id == uuid_id)) + if not result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail=f"Task {task_id} not found") + + job = OptimizationJob( + name=data.name, + config_ids=data.config_ids, + task_ids=data.task_ids, + parallel=data.parallel, + use_llm_eval=data.use_llm_eval, + total_experiments=len(data.config_ids) * len(data.task_ids), + ) + session.add(job) + await session.commit() + await session.refresh(job) + return job + + +@router.get("/{job_id}", response_model=JobResponse) +async def get_job( + job_id: str, + session: AsyncSession = Depends(get_session), +) -> OptimizationJob: + """Get a specific optimization job.""" + uuid_id = parse_uuid(job_id) + result = await session.execute(select(OptimizationJob).where(OptimizationJob.id == uuid_id)) + job = result.scalar_one_or_none() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + return job + + +@router.post("/{job_id}/start") +async def start_job( + job_id: str, + session: AsyncSession = Depends(get_session), +) -> StreamingResponse: + """Start an optimization job and stream progress via SSE.""" + uuid_id = parse_uuid(job_id) + result = await session.execute(select(OptimizationJob).where(OptimizationJob.id == uuid_id)) + job = result.scalar_one_or_none() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status != JobStatus.PENDING: + raise HTTPException(status_code=400, detail=f"Job is already {job.status}") + + async def event_stream() -> AsyncGenerator[str, None]: + service = OptimizerService() + async for progress in service.run_job(job_id): + yield f"data: {progress.model_dump_json()}\n\n" + + return StreamingResponse( + event_stream(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + ) + + +@router.post("/{job_id}/cancel", response_model=JobResponse) +async def cancel_job( + job_id: str, + session: AsyncSession = Depends(get_session), +) -> OptimizationJob: + """Cancel a running optimization job.""" + uuid_id = parse_uuid(job_id) + result = await session.execute(select(OptimizationJob).where(OptimizationJob.id == uuid_id)) + job = result.scalar_one_or_none() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status != JobStatus.RUNNING: + raise HTTPException(status_code=400, detail=f"Job is not running (status: {job.status})") + + # Cancel the running task if it exists + if job_id in _running_jobs: + _running_jobs[job_id].cancel() + del _running_jobs[job_id] + + job.status = JobStatus.CANCELLED + await session.commit() + await session.refresh(job) + return job + + +@router.delete("/{job_id}", status_code=204) +async def delete_job( + job_id: str, + session: AsyncSession = Depends(get_session), +) -> None: + """Delete an optimization job and its runs.""" + uuid_id = parse_uuid(job_id) + result = await session.execute(select(OptimizationJob).where(OptimizationJob.id == uuid_id)) + job = result.scalar_one_or_none() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status == JobStatus.RUNNING: + raise HTTPException(status_code=400, detail="Cannot delete a running job") + + # Runs will be cascade deleted due to foreign key + await session.delete(job) + await session.commit() diff --git a/src/flow/ui/api/runs.py b/src/flow/ui/api/runs.py new file mode 100644 index 0000000000000000000000000000000000000000..60c7b33707938606a02faf36e7079a18e8c55b2e --- /dev/null +++ b/src/flow/ui/api/runs.py @@ -0,0 +1,157 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Run API routes.""" + +from typing import Any +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select, desc + +from ..database import get_session +from ..models.run import ExperimentRun +from ..schemas import RunResponse, RunDetailResponse, CriterionResultSchema + +router = APIRouter(prefix="/runs", tags=["runs"]) + + +def parse_uuid(id_str: str) -> UUID: + """Parse a string to UUID, raising 400 if invalid.""" + try: + return UUID(id_str) + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid UUID: {id_str}") from e + + +@router.get("", response_model=list[RunResponse]) +async def list_runs( + job_id: str | None = None, + config_name: str | None = None, + task_name: str | None = None, + is_pareto: bool | None = None, + session: AsyncSession = Depends(get_session), +) -> list[ExperimentRun]: + """List experiment runs with optional filters.""" + query = select(ExperimentRun) + + if job_id: + uuid_id = parse_uuid(job_id) + query = query.where(ExperimentRun.job_id == uuid_id) + if config_name: + query = query.where(ExperimentRun.config_name == config_name) + if task_name: + query = query.where(ExperimentRun.task_name == task_name) + if is_pareto is not None: + query = query.where(ExperimentRun.is_pareto == is_pareto) + + query = query.order_by(desc(ExperimentRun.created_at)) + result = await session.execute(query) + return list(result.scalars().all()) + + +@router.get("/{run_id}", response_model=RunDetailResponse) +async def get_run( + run_id: str, + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """Get detailed information about a specific run.""" + uuid_id = parse_uuid(run_id) + result = await session.execute(select(ExperimentRun).where(ExperimentRun.id == uuid_id)) + run = result.scalar_one_or_none() + if not run: + raise HTTPException(status_code=404, detail="Run not found") + + # Parse criteria results from trace + criteria_results = [] + if run.trace_json and "criteria_results" in run.trace_json: + for cr in run.trace_json["criteria_results"]: + criteria_results.append(CriterionResultSchema( + name=cr.get("name", ""), + score=cr.get("score", 0.0), + passed=cr.get("passed", False), + reasoning=cr.get("reasoning", ""), + )) + + return { + "id": str(run.id), + "job_id": str(run.job_id), + "config_name": run.config_name, + "task_name": run.task_name, + "status": run.status, + "tokens_total": run.tokens_total, + "tokens_input": run.tokens_input, + "tokens_output": run.tokens_output, + "duration_seconds": run.duration_seconds, + "score": run.score, + "passed": run.passed, + "reasoning": run.reasoning, + "criteria_results": criteria_results, + "output": run.output, + "files_created": run.files_created, + "trace": run.trace_json, + "is_pareto": run.is_pareto, + "pareto_rank": run.pareto_rank, + "created_at": run.created_at, + } + + +@router.get("/job/{job_id}/summary") +async def get_job_summary( + job_id: str, + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """Get aggregated summary for a job's runs.""" + uuid_id = parse_uuid(job_id) + result = await session.execute( + select(ExperimentRun).where(ExperimentRun.job_id == uuid_id) + ) + runs = list(result.scalars().all()) + + if not runs: + raise HTTPException(status_code=404, detail="No runs found for job") + + # Aggregate by config + config_summaries: dict[str, dict[str, Any]] = {} + for run in runs: + if run.config_name not in config_summaries: + config_summaries[run.config_name] = { + "config_name": run.config_name, + "total_runs": 0, + "passed_runs": 0, + "avg_score": 0.0, + "avg_tokens": 0.0, + "avg_duration": 0.0, + "is_pareto": False, + "pareto_rank": 999, + } + + summary = config_summaries[run.config_name] + summary["total_runs"] += 1 + if run.passed: + summary["passed_runs"] += 1 + summary["avg_score"] += run.score + summary["avg_tokens"] += run.tokens_total + summary["avg_duration"] += run.duration_seconds + if run.is_pareto: + summary["is_pareto"] = True + summary["pareto_rank"] = min(summary["pareto_rank"], run.pareto_rank) + + # Calculate averages + for summary in config_summaries.values(): + n = summary["total_runs"] + summary["avg_score"] /= n + summary["avg_tokens"] /= n + summary["avg_duration"] /= n + + # Sort by score descending + sorted_summaries = sorted( + config_summaries.values(), + key=lambda x: (-x["avg_score"], x["avg_tokens"]), + ) + + return { + "job_id": job_id, + "total_runs": len(runs), + "config_summaries": sorted_summaries, + "pareto_configs": [s["config_name"] for s in sorted_summaries if s["is_pareto"]], + } diff --git a/src/flow/ui/api/tasks.py b/src/flow/ui/api/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..9bb1f1c2fc3538874504d1dda35ff46418a4c640 --- /dev/null +++ b/src/flow/ui/api/tasks.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Task API routes.""" + +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select, desc + +from ..database import get_session +from ..models.task import TaskModel +from ..schemas import TaskCreate, TaskResponse + +router = APIRouter(prefix="/tasks", tags=["tasks"]) + + +def parse_uuid(id_str: str) -> UUID: + """Parse a string to UUID, raising 400 if invalid.""" + try: + return UUID(id_str) + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid UUID: {id_str}") from e + + +@router.get("", response_model=list[TaskResponse]) +async def list_tasks( + category: str | None = None, + suite: str | None = None, + session: AsyncSession = Depends(get_session), +) -> list[TaskModel]: + """List all tasks, optionally filtered by category or suite.""" + query = select(TaskModel) + if category: + query = query.where(TaskModel.category == category) + if suite: + query = query.where(TaskModel.suite == suite) + query = query.order_by(desc(TaskModel.created_at)) + result = await session.execute(query) + return list(result.scalars().all()) + + +@router.post("", response_model=TaskResponse, status_code=201) +async def create_task( + data: TaskCreate, + session: AsyncSession = Depends(get_session), +) -> TaskModel: + """Create a new task.""" + task = TaskModel( + name=data.name, + prompt=data.prompt, + criteria_json=data.to_criteria_json(), + category=data.category, + ) + session.add(task) + await session.commit() + await session.refresh(task) + return task + + +@router.get("/{task_id}", response_model=TaskResponse) +async def get_task( + task_id: str, + session: AsyncSession = Depends(get_session), +) -> TaskModel: + """Get a specific task.""" + uuid_id = parse_uuid(task_id) + result = await session.execute(select(TaskModel).where(TaskModel.id == uuid_id)) + task = result.scalar_one_or_none() + if not task: + raise HTTPException(status_code=404, detail="Task not found") + return task + + +@router.delete("/{task_id}", status_code=204) +async def delete_task( + task_id: str, + session: AsyncSession = Depends(get_session), +) -> None: + """Delete a task.""" + uuid_id = parse_uuid(task_id) + result = await session.execute(select(TaskModel).where(TaskModel.id == uuid_id)) + task = result.scalar_one_or_none() + if not task: + raise HTTPException(status_code=404, detail="Task not found") + + await session.delete(task) + await session.commit() + + +@router.post("/import-suite", response_model=list[TaskResponse], status_code=201) +async def import_suite( + suite_name: str, + session: AsyncSession = Depends(get_session), +) -> list[TaskModel]: + """Import tasks from a built-in suite.""" + from flow.experiments.types import get_task_suite + + try: + suite_tasks = get_task_suite(suite_name) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + created_tasks = [] + for t in suite_tasks: + task = TaskModel( + name=t.name, + prompt=t.prompt, + criteria_json=[{"name": c.name, "instruction": c.instruction, "weight": c.weight} for c in t.criteria], + category=t.metadata.get("category", "default"), + suite=suite_name, + ) + session.add(task) + created_tasks.append(task) + + await session.commit() + for task in created_tasks: + await session.refresh(task) + + return created_tasks diff --git a/src/flow/ui/database.py b/src/flow/ui/database.py new file mode 100644 index 0000000000000000000000000000000000000000..5e14f54084b0b2ac96af9393031738da4654a77e --- /dev/null +++ b/src/flow/ui/database.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Database setup with SQLModel and SQLite.""" + +from pathlib import Path +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlmodel import SQLModel + +# Database path +DB_PATH = Path.home() / ".flow" / "flow_ui.db" +DB_PATH.parent.mkdir(parents=True, exist_ok=True) + +DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}" + +engine = create_async_engine(DATABASE_URL, echo=False, future=True) + +async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +async def init_db() -> None: + """Initialize database tables.""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + """Get database session.""" + async with async_session() as session: + yield session diff --git a/src/flow/ui/main.py b/src/flow/ui/main.py new file mode 100644 index 0000000000000000000000000000000000000000..c030a8d8b0a9eb3df39276fb2e6ce336df50a1ff --- /dev/null +++ b/src/flow/ui/main.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft. All rights reserved. +"""FastAPI server for Flow UI.""" + +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any, AsyncGenerator + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse + +from .database import init_db +from .api import configs_router, tasks_router, jobs_router, runs_router + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + """Initialize database on startup.""" + await init_db() + yield + + +app = FastAPI( + title="Flow Optimization UI", + description="Web UI for running agent configuration optimization experiments", + version="0.1.0", + lifespan=lifespan, +) + +# CORS for development +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# API routes +app.include_router(configs_router, prefix="/api") +app.include_router(tasks_router, prefix="/api") +app.include_router(jobs_router, prefix="/api") +app.include_router(runs_router, prefix="/api") + + +# Health check +@app.get("/api/health") +async def health_check() -> dict[str, Any]: + """Health check endpoint.""" + return {"status": "ok", "service": "flow-ui"} + + +# Static files and SPA fallback +# UI is built to backend/ui/ directory so the backend package is self-contained +UI_DIR = Path(__file__).parent / "ui" + + +def setup_static_files() -> None: + """Set up static file serving if frontend is built.""" + if UI_DIR.exists(): + # Serve assets directory + assets_dir = UI_DIR / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str) -> FileResponse: # pyright: ignore[reportUnusedFunction] + """Serve SPA for all non-API routes.""" + file_path = UI_DIR / full_path + if file_path.exists() and file_path.is_file(): + return FileResponse(file_path) + return FileResponse(UI_DIR / "index.html") + + +# Only set up static files if UI is built +if UI_DIR.exists(): + setup_static_files() + + +def run_server(host: str = "0.0.0.0", port: int = 8091) -> None: # noqa: S104 + """Run the FastAPI server.""" + import uvicorn + + uvicorn.run( + "flow.ui.main:app", + host=host, + port=port, + reload=False, + ) + + +if __name__ == "__main__": + run_server() diff --git a/src/flow/ui/models/__init__.py b/src/flow/ui/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b4f28b8ae2b5d59b1809828a528c5d9ab8c0a64c --- /dev/null +++ b/src/flow/ui/models/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Database models.""" + +from .config import AgentConfig +from .task import TaskModel +from .job import OptimizationJob, JobStatus +from .run import ExperimentRun + +__all__ = [ + "AgentConfig", + "TaskModel", + "OptimizationJob", + "JobStatus", + "ExperimentRun", +] diff --git a/src/flow/ui/models/config.py b/src/flow/ui/models/config.py new file mode 100644 index 0000000000000000000000000000000000000000..82b8b149ef51a3259fcaf9da7a02086edf393f17 --- /dev/null +++ b/src/flow/ui/models/config.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Agent configuration model.""" + +from datetime import datetime, timezone +from typing import Any +from uuid import UUID, uuid4 + +from sqlmodel import Field, SQLModel, Column, JSON + + +class AgentConfig(SQLModel, table=True): + """Stored agent configuration.""" + + __tablename__ = "agent_configs" # type: ignore[assignment] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + name: str = Field(index=True) + description: str = "" + + # Store AblationConfig as JSON + config_json: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) + + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + @property + def config(self) -> dict[str, Any]: + """Alias for config_json for API compatibility.""" + return self.config_json diff --git a/src/flow/ui/models/job.py b/src/flow/ui/models/job.py new file mode 100644 index 0000000000000000000000000000000000000000..61213812c341320c9e35f46c943f0fe8d5780899 --- /dev/null +++ b/src/flow/ui/models/job.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Optimization job model.""" + +from datetime import datetime, timezone +from enum import Enum +from uuid import UUID, uuid4 + +from sqlmodel import Field, SQLModel, Column, JSON + + +class JobStatus(str, Enum): + """Job status enum.""" + + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class OptimizationJob(SQLModel, table=True): + """Optimization job tracking.""" + + __tablename__ = "optimization_jobs" # type: ignore[assignment] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + name: str = Field(default="") + + status: JobStatus = Field(default=JobStatus.PENDING) + + # Job configuration + parallel: int = Field(default=4) + use_llm_eval: bool = Field(default=False) + + # Selected configs and tasks (stored as IDs) + config_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON)) + task_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON)) + + # Results + pareto_frontier: list[str] = Field(default_factory=list, sa_column=Column(JSON)) + output_dir: str | None = None + error: str | None = None + + # Progress tracking + total_experiments: int = Field(default=0) + completed_experiments: int = Field(default=0) + + # Timestamps + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + started_at: datetime | None = None + completed_at: datetime | None = None diff --git a/src/flow/ui/models/run.py b/src/flow/ui/models/run.py new file mode 100644 index 0000000000000000000000000000000000000000..463f69dc6b3943693241e5a952dceb2eef432775 --- /dev/null +++ b/src/flow/ui/models/run.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Experiment run model.""" + +from datetime import datetime, timezone +from typing import Any +from uuid import UUID, uuid4 + +from sqlmodel import Field, SQLModel, Column, JSON + + +class ExperimentRun(SQLModel, table=True): + """Individual experiment run (config Γ— task).""" + + __tablename__ = "experiment_runs" # type: ignore[assignment] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + job_id: UUID = Field(foreign_key="optimization_jobs.id", index=True) + + config_name: str + task_name: str + + # Status + status: str = "pending" # pending, running, completed, failed + + # Metrics + tokens_total: int = 0 + tokens_input: int = 0 + tokens_output: int = 0 + duration_seconds: float = 0.0 + + # Evaluation + score: float = 0.0 + passed: bool = False + reasoning: str = "" + criteria_results: list[dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON)) + + # Output + output: str = "" + files_created: list[str] = Field(default_factory=list, sa_column=Column(JSON)) + trace_json: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) + + # Pareto analysis + is_pareto: bool = False + pareto_rank: int = 0 + + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) diff --git a/src/flow/ui/models/task.py b/src/flow/ui/models/task.py new file mode 100644 index 0000000000000000000000000000000000000000..287129925a9788270181b4bcb695328a8228c042 --- /dev/null +++ b/src/flow/ui/models/task.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Task model.""" + +from datetime import datetime, timezone +from typing import Any +from uuid import UUID, uuid4 + +from sqlmodel import Field, SQLModel, Column, JSON + + +class TaskModel(SQLModel, table=True): + """Stored task with evaluation criteria.""" + + __tablename__ = "tasks" # type: ignore[assignment] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + name: str = Field(index=True) + prompt: str + + # Store criteria as JSON list + criteria_json: list[dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON)) + + # Optional metadata + category: str = "default" + suite: str | None = None # If part of a built-in suite + + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + @property + def criteria(self) -> list[dict[str, Any]]: + """Alias for criteria_json for API compatibility.""" + return self.criteria_json diff --git a/src/flow/ui/schemas/__init__.py b/src/flow/ui/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bef267daf3cad7e0db66e7c32219c9652d5d9490 --- /dev/null +++ b/src/flow/ui/schemas/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Pydantic schemas for API requests/responses.""" + +from .config import ConfigCreate, ConfigUpdate, ConfigResponse +from .task import TaskCreate, TaskResponse, CriterionSchema +from .job import JobCreate, JobResponse, JobProgress +from .run import RunResponse, RunDetailResponse, CriterionResultSchema + +__all__ = [ + "ConfigCreate", + "ConfigUpdate", + "ConfigResponse", + "TaskCreate", + "TaskResponse", + "CriterionSchema", + "JobCreate", + "JobResponse", + "JobProgress", + "RunResponse", + "RunDetailResponse", + "CriterionResultSchema", +] diff --git a/src/flow/ui/schemas/config.py b/src/flow/ui/schemas/config.py new file mode 100644 index 0000000000000000000000000000000000000000..4914b6f0ca16726624ff0a0f661373bf3d2c2b45 --- /dev/null +++ b/src/flow/ui/schemas/config.py @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Config schemas.""" + +from datetime import datetime +from typing import Any +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, field_validator + + +class ConfigCreate(BaseModel): + """Request schema for creating a config.""" + + name: str + description: str = "" + enable_message_compaction: bool = True + enable_memory_tool: bool = True + enable_sub_agent: bool = False + compaction_head_size: int = 10 + compaction_tail_size: int = 40 + bash_timeout: int = 120 + + def to_config_json(self) -> dict[str, Any]: + """Convert to config JSON for storage.""" + return { + "name": self.name, + "enable_message_compaction": self.enable_message_compaction, + "enable_memory_tool": self.enable_memory_tool, + "enable_sub_agent": self.enable_sub_agent, + "compaction_head_size": self.compaction_head_size, + "compaction_tail_size": self.compaction_tail_size, + "bash_timeout": self.bash_timeout, + } + + +class ConfigUpdate(BaseModel): + """Request schema for updating a config.""" + + name: str | None = None + description: str | None = None + enable_message_compaction: bool | None = None + enable_memory_tool: bool | None = None + enable_sub_agent: bool | None = None + compaction_head_size: int | None = None + compaction_tail_size: int | None = None + bash_timeout: int | None = None + + +class ConfigResponse(BaseModel): + """Response schema for a config.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + name: str + description: str + config: dict[str, Any] + created_at: datetime + updated_at: datetime + + @field_validator("id", mode="before") + @classmethod + def convert_uuid(cls, v: UUID | str) -> str: + """Convert UUID to string.""" + if isinstance(v, UUID): + return str(v) + return v diff --git a/src/flow/ui/schemas/job.py b/src/flow/ui/schemas/job.py new file mode 100644 index 0000000000000000000000000000000000000000..4abbef131601888e43d536ae40fad7bfc79371de --- /dev/null +++ b/src/flow/ui/schemas/job.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Job schemas.""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, field_validator + +from ..models.job import JobStatus + + +class JobCreate(BaseModel): + """Request schema for creating a job.""" + + name: str = "" + config_ids: list[str] + task_ids: list[str] + parallel: int = 4 + use_llm_eval: bool = False + + +class JobResponse(BaseModel): + """Response schema for a job.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + name: str + status: JobStatus + parallel: int + use_llm_eval: bool + config_ids: list[str] + task_ids: list[str] + pareto_frontier: list[str] + output_dir: str | None + error: str | None + total_experiments: int + completed_experiments: int + created_at: datetime + started_at: datetime | None + completed_at: datetime | None + + @field_validator("id", mode="before") + @classmethod + def convert_uuid(cls, v: UUID | str) -> str: + """Convert UUID to string.""" + if isinstance(v, UUID): + return str(v) + return v + + +class JobProgress(BaseModel): + """Progress event for SSE streaming.""" + + event: str # "progress", "complete", "error" + job_id: str + completed: int = 0 + total: int = 0 + current_config: str = "" + current_task: str = "" + message: str = "" diff --git a/src/flow/ui/schemas/run.py b/src/flow/ui/schemas/run.py new file mode 100644 index 0000000000000000000000000000000000000000..f171b9434e676a18a6a0663e1936d402627edace --- /dev/null +++ b/src/flow/ui/schemas/run.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Run schemas.""" + +from datetime import datetime +from typing import Any +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, field_validator + + +class RunResponse(BaseModel): + """Response schema for a run (summary).""" + + model_config = ConfigDict(from_attributes=True) + + id: str + job_id: str + config_name: str + task_name: str + status: str + tokens_total: int + duration_seconds: float + score: float + passed: bool + is_pareto: bool + pareto_rank: int + created_at: datetime + + @field_validator("id", "job_id", mode="before") + @classmethod + def convert_uuid(cls, v: UUID | str) -> str: + """Convert UUID to string.""" + if isinstance(v, UUID): + return str(v) + return v + + +class CriterionResultSchema(BaseModel): + """Criterion evaluation result.""" + + name: str + score: float + passed: bool + reasoning: str + + +class RunDetailResponse(BaseModel): + """Response schema for run details.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + job_id: str + config_name: str + task_name: str + status: str + + # Metrics + tokens_total: int + tokens_input: int + tokens_output: int + duration_seconds: float + + # Evaluation + score: float + passed: bool + reasoning: str + criteria_results: list[CriterionResultSchema] + + # Output + output: str + files_created: list[str] + trace: dict[str, Any] + + # Pareto + is_pareto: bool + pareto_rank: int + + created_at: datetime + + @field_validator("id", "job_id", mode="before") + @classmethod + def convert_uuid(cls, v: UUID | str) -> str: + """Convert UUID to string.""" + if isinstance(v, UUID): + return str(v) + return v diff --git a/src/flow/ui/schemas/task.py b/src/flow/ui/schemas/task.py new file mode 100644 index 0000000000000000000000000000000000000000..84b5a410f8acfcb5d7b2c3231180d1d6306a3149 --- /dev/null +++ b/src/flow/ui/schemas/task.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Task schemas.""" + +from datetime import datetime +from typing import Any +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, field_validator + + +class CriterionSchema(BaseModel): + """Evaluation criterion schema.""" + + name: str + instruction: str + weight: float = 1.0 + + +class TaskCreate(BaseModel): + """Request schema for creating a task.""" + + name: str + prompt: str + criteria: list[CriterionSchema] = [] + category: str = "default" + + def to_criteria_json(self) -> list[dict[str, Any]]: + """Convert criteria to JSON for storage.""" + return [c.model_dump() for c in self.criteria] + + +class TaskResponse(BaseModel): + """Response schema for a task.""" + + model_config = ConfigDict(from_attributes=True) + + id: str + name: str + prompt: str + criteria: list[CriterionSchema] + category: str + suite: str | None + created_at: datetime + + @field_validator("id", mode="before") + @classmethod + def convert_uuid(cls, v: UUID | str) -> str: + """Convert UUID to string.""" + if isinstance(v, UUID): + return str(v) + return v diff --git a/src/flow/ui/services/__init__.py b/src/flow/ui/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b688893940a4b91a98bad487664c493ef1d2ae0b --- /dev/null +++ b/src/flow/ui/services/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Backend services package.""" + +from .optimizer_service import OptimizerService + +__all__ = ["OptimizerService"] diff --git a/src/flow/ui/services/optimizer_service.py b/src/flow/ui/services/optimizer_service.py new file mode 100644 index 0000000000000000000000000000000000000000..9b8fda05fc5c109e1fd92feee2b75b9367b9beb0 --- /dev/null +++ b/src/flow/ui/services/optimizer_service.py @@ -0,0 +1,228 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Optimizer service that wraps FlowOptimizer for the web UI.""" + +import asyncio +from datetime import datetime, timezone +from typing import AsyncGenerator +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select + +from flow.experiments.ablation import AblationConfig +from flow.experiments.optimizer import FlowOptimizer +from flow.experiments.types import EvalCriterion, Task + +from ..database import async_session +from ..models.config import AgentConfig +from ..models.job import OptimizationJob, JobStatus +from ..models.task import TaskModel +from ..models.run import ExperimentRun +from ..schemas import JobProgress + + +class OptimizerService: + """Service for running optimization jobs.""" + + async def run_job(self, job_id: str | UUID) -> AsyncGenerator[JobProgress, None]: + """Run an optimization job and yield progress updates.""" + async with async_session() as session: + # Load job + result = await session.execute( + select(OptimizationJob).where(OptimizationJob.id == str(job_id)) + ) + job = result.scalar_one_or_none() + if not job: + yield JobProgress( + event="error", + job_id=str(job_id), + message="Job not found", + ) + return + + # Update job status + job.status = JobStatus.RUNNING + job.started_at = datetime.now(timezone.utc) + await session.commit() + + yield JobProgress( + event="progress", + job_id=str(job_id), + completed=0, + total=job.total_experiments, + message="Starting optimization...", + ) + + try: + # Load configs + configs = await self._load_configs(session, job.config_ids) + if not configs: + raise ValueError("No valid configs found") + + # Load tasks + tasks = await self._load_tasks(session, job.task_ids) + if not tasks: + raise ValueError("No valid tasks found") + + # Create optimizer + optimizer = FlowOptimizer( + parallel=job.parallel, + use_llm_evaluator=job.use_llm_eval, + ) + + # Track progress via callback + progress_queue: asyncio.Queue[tuple[int, int, str, str]] = asyncio.Queue() + + def progress_callback(completed: int, total: int, config: str, task: str) -> None: + """Callback invoked by FlowOptimizer on each completion.""" + try: + progress_queue.put_nowait((completed, total, config, task)) + except asyncio.QueueFull: + pass + + # Run optimization in background task + async def run_optimization(): + return await optimizer.optimize( + configs=configs, + tasks=tasks, + progress_callback=progress_callback, + ) + + # Start optimization + opt_task = asyncio.create_task(run_optimization()) + + # Yield progress updates while optimization runs + while not opt_task.done(): + try: + # Wait for progress with timeout + completed, total, config_name, task_name = await asyncio.wait_for( + progress_queue.get(), + timeout=1.0, + ) + yield JobProgress( + event="progress", + job_id=str(job_id), + completed=completed, + total=total, + current_config=config_name, + current_task=task_name, + message=f"Running {config_name}/{task_name}...", + ) + + # Update job progress in DB + job.completed_experiments = completed + await session.commit() + + except asyncio.TimeoutError: + # No progress update, just continue + continue + + # Get final result + opt_result = await opt_task + + # Save runs to database + for summary in opt_result.summaries: + for task_result in summary.task_results: + run = ExperimentRun( + job_id=job.id, + config_name=task_result.config_name, + task_name=task_result.task_name, + status="completed", + tokens_total=task_result.metrics.total_tokens, + tokens_input=task_result.metrics.input_tokens, + tokens_output=task_result.metrics.output_tokens, + duration_seconds=task_result.run_result.duration_seconds, + score=task_result.eval_score, + passed=task_result.eval_passed, + reasoning=task_result.eval_reasoning, + output=task_result.run_result.output or "", + files_created=task_result.run_result.files_created or [], + trace_json={ + "success": task_result.run_result.success, + "error": task_result.run_result.error, + }, + is_pareto=summary.is_pareto_optimal, + pareto_rank=summary.pareto_rank or 0, + ) + session.add(run) + + # Update job + job.status = JobStatus.COMPLETED + job.completed_experiments = opt_result.total_experiments + job.pareto_frontier = opt_result.pareto_frontier + job.output_dir = str(opt_result.output_dir) + job.completed_at = datetime.now(timezone.utc) + await session.commit() + + yield JobProgress( + event="complete", + job_id=str(job_id), + completed=opt_result.total_experiments, + total=job.total_experiments, + message=f"Optimization complete. Pareto configs: {', '.join(opt_result.pareto_frontier)}", + ) + + except Exception as e: + job.status = JobStatus.FAILED + job.error = str(e) + await session.commit() + + yield JobProgress( + event="error", + job_id=str(job_id), + message=f"Optimization failed: {e}", + ) + + async def _load_configs( + self, + session: AsyncSession, + config_ids: list[str], + ) -> list[AblationConfig]: + """Load configs from database and convert to AblationConfig.""" + configs = [] + for config_id in config_ids: + result = await session.execute( + select(AgentConfig).where(AgentConfig.id == config_id) + ) + db_config = result.scalar_one_or_none() + if db_config: + cfg = db_config.config_json + configs.append(AblationConfig( + name=db_config.name, + enable_message_compaction=cfg.get("enable_message_compaction", True), + enable_memory_tool=cfg.get("enable_memory_tool", True), + enable_sub_agent=cfg.get("enable_sub_agent", False), + compaction_head_size=cfg.get("compaction_head_size", 10), + compaction_tail_size=cfg.get("compaction_tail_size", 40), + bash_timeout=cfg.get("bash_timeout", 120), + )) + return configs + + async def _load_tasks( + self, + session: AsyncSession, + task_ids: list[str], + ) -> list[Task]: + """Load tasks from database and convert to Task.""" + tasks = [] + for task_id in task_ids: + result = await session.execute( + select(TaskModel).where(TaskModel.id == task_id) + ) + db_task = result.scalar_one_or_none() + if db_task: + criteria = [ + EvalCriterion( + name=c.get("name", ""), + instruction=c.get("instruction", ""), + weight=c.get("weight", 1.0), + ) + for c in db_task.criteria_json + ] + tasks.append(Task( + name=db_task.name, + prompt=db_task.prompt, + criteria=criteria, + metadata={"category": db_task.category}, + )) + return tasks diff --git a/src/flow/ui/tests/__init__.py b/src/flow/ui/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c84a0262f594f276549658b73a6e195e1d23da4 --- /dev/null +++ b/src/flow/ui/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft. All rights reserved. +"""Tests for Flow UI.""" diff --git a/src/flow/ui/tests/test_e2e_user_journey.py b/src/flow/ui/tests/test_e2e_user_journey.py new file mode 100644 index 0000000000000000000000000000000000000000..0092aed8e17aff9953a2aba7488569b85f103b62 --- /dev/null +++ b/src/flow/ui/tests/test_e2e_user_journey.py @@ -0,0 +1,453 @@ +# Copyright (c) Microsoft. All rights reserved. +"""End-to-end test for the full user journey in Flow UI. + +This test uses ASGI transport to avoid port binding issues and runs through +the complete user journey with proper cleanup. + +Run with: pytest src/flow/ui/tests/test_e2e_user_journey.py -xvs +""" + +from typing import AsyncGenerator + +import httpx +import pytest + +from flow.ui.main import app +from flow.ui.database import init_db + + +@pytest.fixture +async def client() -> AsyncGenerator[httpx.AsyncClient, None]: + """Create an ASGI test client that doesn't require port binding.""" + # Initialize database + await init_db() + + # Use ASGI transport - no actual server needed + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + timeout=30.0, + ) as client: + yield client + + +class TestE2EUserJourney: + """End-to-end tests for the complete user journey.""" + + @pytest.mark.asyncio + async def test_full_user_journey(self, client: httpx.AsyncClient): + """Test the complete user journey from agent creation to job results. + + User Journey: + 1. Health check + 2. Create agents with different configurations + 3. Import a task suite + 4. Create an optimization job + 5. Get job details + 6. List runs (will be empty since we don't actually run the optimizer) + 7. Get job summary + 8. Clean up: delete job, tasks, agents + """ + # Track created resources for cleanup + created_agent_ids: list[str] = [] + created_task_ids: list[str] = [] + created_job_ids: list[str] = [] + + try: + # ======================================== + # STEP 1: Health Check + # ======================================== + print("\n[1] Health Check") + resp = await client.get("/api/health") + assert resp.status_code == 200, f"Health check failed: {resp.text}" + data = resp.json() + assert data["status"] == "ok" + assert data["service"] == "flow-ui" + print(" βœ“ Server is healthy") + + # ======================================== + # STEP 2: Create Agents + # ======================================== + print("\n[2] Create Agents") + + # Agent 1: Baseline (no compaction) + agent1_data = { + "name": "e2e-baseline", + "description": "Baseline agent without compaction", + "enable_message_compaction": False, + "enable_memory_tool": True, + "enable_sub_agent": False, + "bash_timeout": 120, + } + resp = await client.post("/api/configs", json=agent1_data) + assert resp.status_code == 201, f"Failed to create agent 1: {resp.text}" + agent1 = resp.json() + created_agent_ids.append(agent1["id"]) + print(f" βœ“ Created agent 1: {agent1['name']} (id: {agent1['id'][:8]}...)") + + # Agent 2: With compaction + agent2_data = { + "name": "e2e-compaction", + "description": "Agent with message compaction enabled", + "enable_message_compaction": True, + "compaction_head_size": 10, + "compaction_tail_size": 40, + "enable_memory_tool": True, + "enable_sub_agent": False, + "bash_timeout": 120, + } + resp = await client.post("/api/configs", json=agent2_data) + assert resp.status_code == 201, f"Failed to create agent 2: {resp.text}" + agent2 = resp.json() + created_agent_ids.append(agent2["id"]) + print(f" βœ“ Created agent 2: {agent2['name']} (id: {agent2['id'][:8]}...)") + + # Verify agents were created + resp = await client.get("/api/configs") + assert resp.status_code == 200 + agents = resp.json() + agent_names = [a["name"] for a in agents] + assert "e2e-baseline" in agent_names + assert "e2e-compaction" in agent_names + print(f" βœ“ Verified {len(agents)} agents exist") + + # ======================================== + # STEP 3: Import Task Suite + # ======================================== + print("\n[3] Import Task Suite") + + resp = await client.post("/api/tasks/import-suite?suite_name=quick") + assert resp.status_code == 201, f"Failed to import suite: {resp.text}" + tasks = resp.json() + for task in tasks: + created_task_ids.append(task["id"]) + print(f" βœ“ Imported 'quick' suite with {len(tasks)} tasks") + for task in tasks: + print(f" - {task['name']} (id: {task['id'][:8]}...)") + + # Verify tasks exist + resp = await client.get("/api/tasks") + assert resp.status_code == 200 + all_tasks = resp.json() + print(f" βœ“ Total tasks in database: {len(all_tasks)}") + + # ======================================== + # STEP 4: Create Optimization Job + # ======================================== + print("\n[4] Create Optimization Job") + + job_data = { + "name": "E2E Test Optimization", + "config_ids": created_agent_ids, + "task_ids": created_task_ids[:2], # Use first 2 tasks + "parallel": 2, + "use_llm_eval": False, + } + resp = await client.post("/api/jobs", json=job_data) + assert resp.status_code == 201, f"Failed to create job: {resp.text}" + job = resp.json() + created_job_ids.append(job["id"]) + print(f" βœ“ Created job: {job['name']} (id: {job['id'][:8]}...)") + print(f" - Status: {job['status']}") + print(f" - Total experiments: {job['total_experiments']}") + print(f" - Configs: {len(job['config_ids'])}, Tasks: {len(job['task_ids'])}") + + # ======================================== + # STEP 5: Get Job Details + # ======================================== + print("\n[5] Get Job Details") + + resp = await client.get(f"/api/jobs/{job['id']}") + assert resp.status_code == 200, f"Failed to get job: {resp.text}" + job_detail = resp.json() + assert job_detail["id"] == job["id"] + assert job_detail["status"] == "pending" + print(f" βœ“ Retrieved job details") + print(f" - Name: {job_detail['name']}") + print(f" - Status: {job_detail['status']}") + print(f" - Created: {job_detail['created_at']}") + + # ======================================== + # STEP 6: List Runs (empty before job starts) + # ======================================== + print("\n[6] List Runs") + + resp = await client.get("/api/runs", params={"job_id": job["id"]}) + assert resp.status_code == 200, f"Failed to list runs: {resp.text}" + runs = resp.json() + print(f" βœ“ Found {len(runs)} runs (expected 0 before job starts)") + assert len(runs) == 0, "Should have no runs before job starts" + + # ======================================== + # STEP 7: Get Job Summary (will return error since no runs) + # ======================================== + print("\n[7] Get Job Summary") + + resp = await client.get(f"/api/runs/job/{job['id']}/summary") + # Should return 404 or error since no runs exist + print(f" βœ“ Job summary response: {resp.status_code}") + if resp.status_code == 404: + print(" (Expected: no runs yet for summary)") + + # ======================================== + # STEP 8: Test Additional Endpoints + # ======================================== + print("\n[8] Test Additional Endpoints") + + # Get single agent + resp = await client.get(f"/api/configs/{agent1['id']}") + assert resp.status_code == 200 + print(f" βœ“ GET /configs/{{id}} works") + + # Get single task + resp = await client.get(f"/api/tasks/{created_task_ids[0]}") + assert resp.status_code == 200 + print(f" βœ“ GET /tasks/{{id}} works") + + # List jobs + resp = await client.get("/api/jobs") + assert resp.status_code == 200 + print(f" βœ“ GET /jobs works ({len(resp.json())} jobs)") + + # ======================================== + # STEP 9: Cleanup + # ======================================== + print("\n[9] Cleanup") + + # Delete jobs + for job_id in created_job_ids: + resp = await client.delete(f"/api/jobs/{job_id}") + assert resp.status_code == 204, f"Failed to delete job {job_id}: {resp.text}" + print(f" βœ“ Deleted job {job_id[:8]}...") + + # Verify job deleted + resp = await client.get(f"/api/jobs/{created_job_ids[0]}") + assert resp.status_code == 404 + print(" βœ“ Verified job deletion (404)") + + # Delete tasks + for task_id in created_task_ids: + resp = await client.delete(f"/api/tasks/{task_id}") + assert resp.status_code == 204, f"Failed to delete task {task_id}: {resp.text}" + print(f" βœ“ Deleted {len(created_task_ids)} tasks") + + # Delete agents + for agent_id in created_agent_ids: + resp = await client.delete(f"/api/configs/{agent_id}") + assert resp.status_code == 204, f"Failed to delete agent {agent_id}: {resp.text}" + print(f" βœ“ Deleted {len(created_agent_ids)} agents") + + # ======================================== + # Final Summary + # ======================================== + print("\n" + "=" * 50) + print("E2E USER JOURNEY TEST PASSED!") + print("=" * 50) + print(f" - Created and deleted {len(created_agent_ids)} agents") + print(f" - Imported and deleted {len(created_task_ids)} tasks") + print(f" - Created and deleted {len(created_job_ids)} jobs") + print(" - All API endpoints verified working") + print("") + + except Exception as e: + # Cleanup on failure + print(f"\n[ERROR] Test failed: {e}") + print("Attempting cleanup...") + try: + for job_id in created_job_ids: + await client.delete(f"/api/jobs/{job_id}") + for task_id in created_task_ids: + await client.delete(f"/api/tasks/{task_id}") + for agent_id in created_agent_ids: + await client.delete(f"/api/configs/{agent_id}") + print("Cleanup completed") + except Exception as cleanup_error: + print(f"Cleanup failed: {cleanup_error}") + raise + + @pytest.mark.asyncio + async def test_error_handling(self, client: httpx.AsyncClient): + """Test API error handling for invalid inputs.""" + print("\n[Error Handling Tests]") + + # Test invalid UUID + resp = await client.get("/api/configs/invalid-uuid") + assert resp.status_code == 400 + assert "Invalid UUID" in resp.json()["detail"] + print(" βœ“ Invalid UUID returns 400") + + # Test non-existent resource + resp = await client.get("/api/configs/00000000-0000-0000-0000-000000000000") + assert resp.status_code == 404 + print(" βœ“ Non-existent resource returns 404") + + # Test creating job with non-existent config + job_data = { + "name": "Invalid Job", + "config_ids": ["00000000-0000-0000-0000-000000000000"], + "task_ids": ["00000000-0000-0000-0000-000000000001"], + } + resp = await client.post("/api/jobs", json=job_data) + assert resp.status_code == 400 + assert "not found" in resp.json()["detail"] + print(" βœ“ Job with invalid config returns 400") + + # Test importing invalid suite + resp = await client.post("/api/tasks/import-suite?suite_name=nonexistent") + assert resp.status_code == 400 + print(" βœ“ Invalid suite name returns 400") + + print("\n All error handling tests passed!") + + +class TestAPIEndpoints: + """Unit tests for individual API endpoints.""" + + @pytest.mark.asyncio + async def test_health_endpoint(self, client: httpx.AsyncClient): + """Test health check endpoint.""" + resp = await client.get("/api/health") + assert resp.status_code == 200 + data = resp.json() + assert data["status"] == "ok" + + @pytest.mark.asyncio + async def test_configs_crud(self, client: httpx.AsyncClient): + """Test full CRUD for configs/agents.""" + # Create + resp = await client.post( + "/api/configs", + json={"name": "test-crud", "description": "CRUD test"}, + ) + assert resp.status_code == 201 + config = resp.json() + config_id = config["id"] + + try: + # Read + resp = await client.get(f"/api/configs/{config_id}") + assert resp.status_code == 200 + assert resp.json()["name"] == "test-crud" + + # List + resp = await client.get("/api/configs") + assert resp.status_code == 200 + assert any(c["id"] == config_id for c in resp.json()) + + # Delete + resp = await client.delete(f"/api/configs/{config_id}") + assert resp.status_code == 204 + + # Verify deleted + resp = await client.get(f"/api/configs/{config_id}") + assert resp.status_code == 404 + except Exception: + # Cleanup on failure + await client.delete(f"/api/configs/{config_id}") + raise + + @pytest.mark.asyncio + async def test_tasks_crud(self, client: httpx.AsyncClient): + """Test full CRUD for tasks.""" + # Create + resp = await client.post( + "/api/tasks", + json={ + "name": "test-task", + "prompt": "Test prompt", + "category": "test", + "criteria": [{"name": "test", "instruction": "Test it", "weight": 1.0}], + }, + ) + assert resp.status_code == 201 + task = resp.json() + task_id = task["id"] + + try: + # Read + resp = await client.get(f"/api/tasks/{task_id}") + assert resp.status_code == 200 + assert resp.json()["name"] == "test-task" + + # List + resp = await client.get("/api/tasks") + assert resp.status_code == 200 + + # Delete + resp = await client.delete(f"/api/tasks/{task_id}") + assert resp.status_code == 204 + except Exception: + # Cleanup on failure + await client.delete(f"/api/tasks/{task_id}") + raise + + @pytest.mark.asyncio + async def test_jobs_crud(self, client: httpx.AsyncClient): + """Test full CRUD for jobs.""" + # First create a config and task + config_resp = await client.post( + "/api/configs", + json={"name": "job-test-config"}, + ) + config = config_resp.json() + + task_resp = await client.post( + "/api/tasks", + json={"name": "job-test-task", "prompt": "Test"}, + ) + task = task_resp.json() + + try: + # Create job + resp = await client.post( + "/api/jobs", + json={ + "name": "test-job", + "config_ids": [config["id"]], + "task_ids": [task["id"]], + }, + ) + assert resp.status_code == 201 + job = resp.json() + job_id = job["id"] + + # Read + resp = await client.get(f"/api/jobs/{job_id}") + assert resp.status_code == 200 + assert resp.json()["name"] == "test-job" + assert resp.json()["status"] == "pending" + + # List + resp = await client.get("/api/jobs") + assert resp.status_code == 200 + assert any(j["id"] == job_id for j in resp.json()) + + # Delete + resp = await client.delete(f"/api/jobs/{job_id}") + assert resp.status_code == 204 + + # Verify deleted + resp = await client.get(f"/api/jobs/{job_id}") + assert resp.status_code == 404 + finally: + # Cleanup + await client.delete(f"/api/tasks/{task['id']}") + await client.delete(f"/api/configs/{config['id']}") + + @pytest.mark.asyncio + async def test_runs_list_and_filter(self, client: httpx.AsyncClient): + """Test runs listing and filtering.""" + # List all runs + resp = await client.get("/api/runs") + assert resp.status_code == 200 + assert isinstance(resp.json(), list) + + # Filter by non-existent job + resp = await client.get("/api/runs", params={"job_id": "00000000-0000-0000-0000-000000000000"}) + assert resp.status_code == 200 + assert resp.json() == [] + + +if __name__ == "__main__": + # Run tests directly + pytest.main([__file__, "-xvs"]) diff --git a/src/flow/ui/ui/.gitignore b/src/flow/ui/ui/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0656d0b84561fbc4a91737c5161b09bd1a60abaf --- /dev/null +++ b/src/flow/ui/ui/.gitignore @@ -0,0 +1,4 @@ +# Built frontend assets - regenerate with: cd app/frontend && yarn build +* +!.gitignore +!.gitkeep diff --git a/src/flow/ui/ui/.gitkeep b/src/flow/ui/ui/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..aca165593499482d10c4cce9c730612141ebbee9 --- /dev/null +++ b/src/flow/ui/ui/.gitkeep @@ -0,0 +1,2 @@ +# This directory contains the built frontend UI +# Run `yarn build` from app/frontend to populate it diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..d867341b4303aa51515bc15c4a832e61af59e319 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1967 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "agent-framework-core" +version = "1.0.0b260116" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "mcp", extra = ["ws"] }, + { name = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/45/ba6cc2b61603bf64dff333131bdea2cbce31309f7da35c608e6182ec1695/agent_framework_core-1.0.0b260116.tar.gz", hash = "sha256:f8db80765a2460721ad1e752a78c63390d3ada174dc55b06b55771e46e619366", size = 300322, upload-time = "2026-01-16T21:31:50.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/35/b757c8b7446167ee20760b00c9b0f98ce9565d1e9fb1b86cba2ad3ecc7f8/agent_framework_core-1.0.0b260116-py3-none-any.whl", hash = "sha256:b8aecf5146e42e56fd01517f339db507290bd34ca907627042fdbbcbcc2ce435", size = 346138, upload-time = "2026-01-16T21:32:10.498Z" }, +] + +[[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.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "azure-core" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flow-agent" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-identity" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +all = [ + { name = "beautifulsoup4" }, + { name = "html2text" }, +] +dev = [ + { name = "mypy" }, + { name = "poethepoet" }, + { name = "pre-commit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +research = [ + { name = "beautifulsoup4" }, + { name = "html2text" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", specifier = ">=1.0.0b0" }, + { name = "azure-identity", specifier = ">=1.15.0" }, + { name = "beautifulsoup4", marker = "extra == 'research'", specifier = ">=4.12.0" }, + { name = "flow-agent", extras = ["research"], marker = "extra == 'all'" }, + { name = "html2text", marker = "extra == 'research'", specifier = ">=2024.2.26" }, + { name = "httpx", specifier = ">=0.25.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, + { name = "opentelemetry-api", specifier = ">=1.20.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.20.0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.41b0" }, + { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.24.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.350" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2.0" }, + { name = "typer", specifier = ">=0.9.0" }, +] +provides-extras = ["research", "all", "dev"] + +[[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 = "html2text" +version = "2025.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/27/e158d86ba1e82967cc2f790b0cb02030d4a8bef58e0c79a8590e9678107f/html2text-2025.4.15.tar.gz", hash = "sha256:948a645f8f0bc3abe7fd587019a2197a12436cd73d0d4908af95bfc8da337588", size = 64316, upload-time = "2025-04-15T04:02:30.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/84/1a0f9555fd5f2b1c924ff932d99b40a0f8a6b12f6dd625e2a47f415b00ea/html2text-2025.4.15-py3-none-any.whl", hash = "sha256:00569167ffdab3d7767a4cdf589b7f57e777a5ed28d12907d8c58769ec734acc", size = 34656, upload-time = "2025-04-15T04:02:28.44Z" }, +] + +[[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 = "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 = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, +] + +[package.optional-dependencies] +ws = [ + { name = "websockets" }, +] + +[[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 = "msal" +version = "1.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "openai" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368, upload-time = "2025-08-22T10:14:17.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080, upload-time = "2025-08-22T10:14:16.477Z" }, +] + +[[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 = "pastel" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[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 = "poethepoet" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pastel" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/9d/054c8435b03324ed9abd5d5ab8c45065b1f42c23952cd23f13a5921d8465/poethepoet-0.40.0.tar.gz", hash = "sha256:91835f00d03d6c4f0e146f80fa510e298ad865e7edd27fe4cb9c94fdc090791b", size = 81114, upload-time = "2026-01-05T19:09:13.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/bc/73327d12b176abea7a3c6c7d760e1a953992f7b59d72c0354e39d7a353b5/poethepoet-0.40.0-py3-none-any.whl", hash = "sha256:afd276ae31d5c53573c0c14898118d4848ccee3709b6b0be6a1c6cbe522bbc8a", size = 106672, upload-time = "2026-01-05T19:09:11.536Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[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 = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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 = "soupsieve" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/f2/21d6ca70c3cf35d01ae9e01be534bf6b6b103c157a728082a5028350c310/soupsieve-2.8.2.tar.gz", hash = "sha256:78a66b0fdee2ab40b7199dc3e747ee6c6e231899feeaae0b9b98a353afd48fd8", size = 118601, upload-time = "2026-01-18T16:21:31.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/9a/b4450ccce353e2430621b3bb571899ffe1033d5cd72c9e065110f95b1a63/soupsieve-2.8.2-py3-none-any.whl", hash = "sha256:0f4c2f6b5a5fb97a641cf69c0bd163670a0e45e6d6c01a2107f93a6a6f93c51a", size = 37016, upload-time = "2026-01-18T16:21:29.7Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.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/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[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" }, +]