From cffed146a018aa1db4228ab7a90da3d210492d7e Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Tue, 17 Mar 2026 19:21:37 +0200 Subject: [PATCH 01/17] feat: Add exgentic_a2a_runner test harness Implement complete test harness for Exgentic benchmarks following the flow described in https://github.com/kagenti/kagenti/issues/963 Key features: - MCP client using official Python SDK with streamable HTTP transport - Sequential session processing with full lifecycle management - A2A protocol integration for agent communication - OpenTelemetry instrumentation for metrics and tracing - Comprehensive configuration and documentation Components: - mcp_client.py: MCP protocol client for Exgentic server - exgentic_adapter.py: High-level adapter for session management - runner.py: Main orchestration with telemetry - config.py: Configuration management - prompt.py: Prompt builder with session_id injection - otel.py: OpenTelemetry setup - a2a_client.py: A2A protocol client (from appworld_a2a_runner) Testing: - Successfully connects to Exgentic MCP server (tau2 benchmark) - Verified session creation with 114 available tasks - Proper error handling and logging configuration Documentation: - README.md: Complete usage guide - QUICKSTART.md: Quick start for Kagenti cluster - Architecture and implementation docs Signed-off-by: Yoav Katz --- EXGENTIC_A2A_RUNNER_PLAN.md | 315 ++++ EXGENTIC_ARCHITECTURE.md | 211 +++ IMPLEMENTATION_SUMMARY.md | 284 ++++ exgentic_a2a_runner/.gitignore | 47 + .../IMPLEMENTATION_CHECKLIST.md | 208 +++ exgentic_a2a_runner/QUICKSTART.md | 146 ++ exgentic_a2a_runner/README.md | 270 ++++ exgentic_a2a_runner/example.env | 78 + .../exgentic_a2a_runner/__init__.py | 9 + .../exgentic_a2a_runner/a2a_client.py | 334 ++++ .../exgentic_a2a_runner/config.py | 142 ++ .../exgentic_a2a_runner/exgentic_adapter.py | 164 ++ .../exgentic_a2a_runner/mcp_client.py | 247 +++ .../exgentic_a2a_runner/otel.py | 372 +++++ .../exgentic_a2a_runner/prompt.py | 31 + .../exgentic_a2a_runner/runner.py | 365 +++++ exgentic_a2a_runner/pyproject.toml | 33 + exgentic_a2a_runner/run-with-port-forward.sh | 132 ++ exgentic_a2a_runner/uv.lock | 1406 +++++++++++++++++ 19 files changed, 4794 insertions(+) create mode 100644 EXGENTIC_A2A_RUNNER_PLAN.md create mode 100644 EXGENTIC_ARCHITECTURE.md create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 exgentic_a2a_runner/.gitignore create mode 100644 exgentic_a2a_runner/IMPLEMENTATION_CHECKLIST.md create mode 100644 exgentic_a2a_runner/QUICKSTART.md create mode 100644 exgentic_a2a_runner/README.md create mode 100644 exgentic_a2a_runner/example.env create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/__init__.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/config.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/otel.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/prompt.py create mode 100644 exgentic_a2a_runner/exgentic_a2a_runner/runner.py create mode 100644 exgentic_a2a_runner/pyproject.toml create mode 100755 exgentic_a2a_runner/run-with-port-forward.sh create mode 100644 exgentic_a2a_runner/uv.lock diff --git a/EXGENTIC_A2A_RUNNER_PLAN.md b/EXGENTIC_A2A_RUNNER_PLAN.md new file mode 100644 index 0000000..a0f1551 --- /dev/null +++ b/EXGENTIC_A2A_RUNNER_PLAN.md @@ -0,0 +1,315 @@ +# Exgentic A2A Runner - Implementation Plan + +## Overview + +Create a test harness called `exgentic_a2a_runner` that integrates Exgentic benchmarks with Kagenti agents using the A2A protocol. This harness will follow the execution model defined in [GitHub Issue #963](https://github.com/kagenti/kagenti/issues/963). + +## Architecture + +### High-Level Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Exgentic A2A Runner │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Runner │───▶│ Exgentic │───▶│ MCP Client │ │ +│ │ │ │ Adapter │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ │ │ +│ │ │ ▼ │ +│ │ │ ┌──────────────┐ │ +│ │ │ │ Exgentic MCP │ │ +│ │ │ │ Server │ │ +│ │ │ └──────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ A2A Client │ │ Prompt │ │ +│ │ │ │ Builder │ │ +│ └──────────────┘ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ Kagenti Agent│ │ +│ │ (via A2A) │ │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Execution Model (from Issue #963) + +For each task: +1. **Create Session**: `(session_id, task) = benchmark_mcp.call("create_session")` +2. **Record Start Time**: `startTime = time()` +3. **Invoke Agent**: `agent.invoke_agent("{task}. Use session id {session_id} in all accesses")` +4. **Record Completion Time**: `completionTime = time() - startTime` +5. **Evaluate Session**: `success = benchmark_mcp.call("evaluate_session", {"session_id": session_id})` +6. **Store Statistics**: `stats[session_id] = (completion_time, success)` +7. **Close Session**: `benchmark_mcp.call("close_session", {"session_id": session_id})` + +## Directory Structure + +``` +exgentic_a2a_runner/ +├── pyproject.toml # Project configuration and dependencies +├── README.md # Documentation +├── example.env # Example environment configuration +├── .gitignore # Git ignore patterns +└── exgentic_a2a_runner/ + ├── __init__.py # Package initialization + ├── runner.py # Main orchestration logic + ├── config.py # Configuration management + ├── exgentic_adapter.py # Exgentic MCP server adapter + ├── mcp_client.py # MCP protocol client + ├── a2a_client.py # A2A protocol client (reused from appworld) + ├── prompt.py # Prompt construction + └── otel.py # OpenTelemetry instrumentation +``` + +## Component Details + +### 1. Configuration (`config.py`) + +**ExgenticConfig** +- `mcp_server_url`: URL of the Exgentic MCP server (required) +- `mcp_timeout_seconds`: Timeout for MCP operations (default: 60) +- `max_tasks`: Maximum number of tasks to process (optional) +- `abort_on_failure`: Stop on first failure (default: false) + +**A2AConfig** (reused from appworld_a2a_runner) +- `base_url`: A2A endpoint base URL +- `timeout_seconds`: Request timeout +- `auth_token`: Bearer token for authentication +- `verify_tls`: TLS verification flag +- `endpoint_path`: Endpoint path + +**OTELConfig** (reused from appworld_a2a_runner) +- Standard OpenTelemetry configuration + +**DebugConfig** (reused from appworld_a2a_runner) +- `log_prompt`: Log prompt details +- `log_response`: Log response details + +### 2. MCP Client (`mcp_client.py`) + +Uses the official MCP Python SDK to communicate with the Exgentic MCP server. + +**Key Methods:** +- `create_session() -> (session_id: str, task: str)`: Create a new benchmark session +- `evaluate_session(session_id: str) -> bool`: Evaluate session success +- `close_session(session_id: str) -> None`: Close and cleanup session + +**Implementation Notes:** +- Use `mcp` Python package for MCP protocol communication +- Handle connection lifecycle properly +- Implement proper error handling and timeouts +- Support both stdio and SSE transport modes + +### 3. Exgentic Adapter (`exgentic_adapter.py`) + +Provides high-level interface to Exgentic MCP server operations. + +**SessionData Class:** +```python +class SessionData: + session_id: str + task: str + created_at: float +``` + +**ExgenticAdapter Class:** +- `initialize()`: Initialize MCP client connection +- `create_session() -> SessionData`: Create new session and get task +- `evaluate_session(session_id: str) -> bool`: Evaluate session +- `close_session(session_id: str)`: Close session +- `iterate_sessions()`: Iterator for sequential session processing + +### 4. Prompt Builder (`prompt.py`) + +Constructs prompts that include the session_id for the agent. + +**Format:** +``` +The task you are to complete is: +{task} + +IMPORTANT: Use session id "{session_id}" in all your interactions with the benchmark tools. +``` + +### 5. A2A Client (`a2a_client.py`) + +Reuse the existing implementation from `appworld_a2a_runner` with minimal modifications. + +### 6. Runner (`runner.py`) + +Main orchestration logic following the execution model. + +**SessionResult Class:** +```python +class SessionResult: + session_id: str + success: bool + latency_ms: float + error: Optional[str] + response_chars: Optional[int] +``` + +**Runner Class:** +- `initialize()`: Initialize all components +- `process_session(session_data: SessionData) -> SessionResult`: Process single session +- `run() -> int`: Main execution loop + +**Process Flow:** +```python +def process_session(session_data): + start_time = time.time() + + # Build prompt with session_id + prompt = build_prompt(session_data.task, session_data.session_id) + + # Send to agent via A2A + response = a2a_client.send_prompt(prompt) + + # Evaluate session + success = exgentic_adapter.evaluate_session(session_data.session_id) + + # Close session + exgentic_adapter.close_session(session_data.session_id) + + completion_time = time.time() - start_time + + return SessionResult( + session_id=session_data.session_id, + success=success, + latency_ms=completion_time * 1000, + response_chars=len(response) + ) +``` + +### 7. OpenTelemetry Instrumentation (`otel.py`) + +Extended from appworld_a2a_runner with additional metrics. + +**Additional Spans:** +- `exgentic_a2a.session`: Overall session processing +- `exgentic_a2a.mcp.create_session`: Session creation +- `exgentic_a2a.mcp.evaluate_session`: Session evaluation +- `exgentic_a2a.mcp.close_session`: Session cleanup + +**Additional Attributes:** +- `exgentic.session_id`: Session identifier +- `exgentic.mcp_server_url`: MCP server URL +- `exgentic.evaluation_result`: Success/failure of evaluation + +**Additional Metrics:** +- `exgentic_a2a_sessions_total{status=success|failed}`: Total sessions processed +- `exgentic_a2a_session_latency_ms`: End-to-end session latency +- `exgentic_a2a_evaluation_latency_ms`: Evaluation operation latency +- `exgentic_a2a_session_creation_latency_ms`: Session creation latency + +## Configuration Files + +### pyproject.toml + +```toml +[project] +name = "exgentic-a2a-runner" +version = "0.1.0" +description = "Exgentic Benchmark A2A Runner for Kagenti" +requires-python = ">=3.11" +dependencies = [ + "mcp>=0.9.0", + "requests>=2.28.0", + "opentelemetry-api>=1.20.0", + "opentelemetry-sdk>=1.20.0", + "opentelemetry-exporter-otlp>=1.20.0", + "opentelemetry-instrumentation-requests>=0.41b0", +] + +[project.scripts] +exgentic-a2a-runner = "exgentic_a2a_runner.runner:main" +``` + +### example.env + +```bash +# REQUIRED CONFIGURATION +EXGENTIC_MCP_SERVER_URL=http://localhost:3000 +A2A_BASE_URL=http://localhost:8000 + +# OPTIONAL CONFIGURATION +EXGENTIC_MCP_TIMEOUT_SECONDS=60 +MAX_TASKS=10 +ABORT_ON_FAILURE=false + +# A2A Configuration +A2A_TIMEOUT_SECONDS=300 +A2A_AUTH_TOKEN= +A2A_VERIFY_TLS=true +A2A_ENDPOINT_PATH=/v1/chat + +# OpenTelemetry Configuration +OTEL_SERVICE_NAME=exgentic-a2a-runner +OTEL_EXPORTER_OTLP_ENDPOINT= +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +OTEL_INSTRUMENT_REQUESTS=true + +# Debug Configuration +LOG_PROMPT=0 +LOG_RESPONSE=0 +``` + +## Key Differences from AppWorld Runner + +1. **MCP Integration**: Uses MCP protocol instead of direct AppWorld API calls +2. **Session Management**: Explicit session lifecycle (create → use → evaluate → close) +3. **Task Source**: Tasks come from MCP server's `create_session` call, not from dataset enumeration +4. **Evaluation**: Uses MCP server's `evaluate_session` instead of AppWorld's evaluation system +5. **Prompt Format**: Includes session_id in the prompt for agent to use +6. **Dependencies**: Adds MCP Python SDK, removes AppWorld package + +## Implementation Phases + +### Phase 1: Core Structure ✓ +- [x] Create directory structure +- [x] Set up configuration management +- [x] Create basic project files (pyproject.toml, README.md, example.env) + +### Phase 2: MCP Integration +- [ ] Implement MCPClient using official MCP SDK +- [ ] Implement ExgenticAdapter with session lifecycle +- [ ] Add proper error handling and timeouts + +### Phase 3: Runner Logic +- [ ] Implement main Runner class +- [ ] Implement session processing flow +- [ ] Add summary statistics and reporting + +### Phase 4: Integration +- [ ] Reuse/adapt A2A client from appworld_a2a_runner +- [ ] Implement prompt builder with session_id +- [ ] Add OpenTelemetry instrumentation + +### Phase 5: Testing & Documentation +- [ ] Test with actual Exgentic MCP server +- [ ] Complete README with usage examples +- [ ] Add error handling and edge cases + +## Success Criteria + +1. ✅ Sequential execution of benchmark tasks via MCP server +2. ✅ Proper session lifecycle management (create → evaluate → close) +3. ✅ Integration with Kagenti agents via A2A protocol +4. ✅ Session_id included in prompts for agent use +5. ✅ Comprehensive OpenTelemetry instrumentation +6. ✅ Configuration via environment variables +7. ✅ Summary statistics and reporting +8. ✅ Proper error handling and logging + +## Next Steps + +1. Review and approve this plan +2. Switch to Code mode to implement the solution +3. Test with actual Exgentic MCP server and Kagenti agent +4. Iterate based on testing results \ No newline at end of file diff --git a/EXGENTIC_ARCHITECTURE.md b/EXGENTIC_ARCHITECTURE.md new file mode 100644 index 0000000..50b4050 --- /dev/null +++ b/EXGENTIC_ARCHITECTURE.md @@ -0,0 +1,211 @@ +# Exgentic A2A Runner - Architecture Diagram + +## System Architecture + +```mermaid +graph TB + subgraph "Exgentic A2A Runner" + Runner[Runner
Main Orchestrator] + ExgenticAdapter[Exgentic Adapter
Session Management] + MCPClient[MCP Client
MCP Protocol] + A2AClient[A2A Client
A2A Protocol] + PromptBuilder[Prompt Builder
Task Formatting] + OTEL[OTEL Instrumentation
Telemetry] + Config[Configuration
Environment Variables] + end + + subgraph "External Services" + ExgenticMCP[Exgentic MCP Server
Benchmark Provider] + KagentiAgent[Kagenti Agent
A2A Endpoint] + OTELCollector[OTEL Collector
Telemetry Backend] + end + + Runner --> Config + Runner --> ExgenticAdapter + Runner --> A2AClient + Runner --> PromptBuilder + Runner --> OTEL + + ExgenticAdapter --> MCPClient + MCPClient --> ExgenticMCP + A2AClient --> KagentiAgent + OTEL --> OTELCollector + + style Runner fill:#e1f5ff + style ExgenticAdapter fill:#fff4e1 + style MCPClient fill:#ffe1f5 + style A2AClient fill:#e1ffe1 + style ExgenticMCP fill:#ffcccc + style KagentiAgent fill:#ccffcc +``` + +## Sequence Diagram - Session Processing + +```mermaid +sequenceDiagram + participant Runner + participant ExgenticAdapter + participant MCPClient + participant ExgenticMCP + participant PromptBuilder + participant A2AClient + participant KagentiAgent + participant OTEL + + Runner->>OTEL: Start session span + Runner->>ExgenticAdapter: create_session() + ExgenticAdapter->>MCPClient: call_tool("create_session") + MCPClient->>ExgenticMCP: MCP Request: create_session + ExgenticMCP-->>MCPClient: {session_id, task} + MCPClient-->>ExgenticAdapter: SessionData + ExgenticAdapter-->>Runner: SessionData(session_id, task) + + Runner->>PromptBuilder: build_prompt(task, session_id) + PromptBuilder-->>Runner: formatted_prompt + + Runner->>A2AClient: send_prompt(prompt) + A2AClient->>KagentiAgent: A2A Request + KagentiAgent-->>A2AClient: Response + A2AClient-->>Runner: response_text + + Runner->>ExgenticAdapter: evaluate_session(session_id) + ExgenticAdapter->>MCPClient: call_tool("evaluate_session", {session_id}) + MCPClient->>ExgenticMCP: MCP Request: evaluate_session + ExgenticMCP-->>MCPClient: {success: true/false} + MCPClient-->>ExgenticAdapter: evaluation_result + ExgenticAdapter-->>Runner: success + + Runner->>ExgenticAdapter: close_session(session_id) + ExgenticAdapter->>MCPClient: call_tool("close_session", {session_id}) + MCPClient->>ExgenticMCP: MCP Request: close_session + ExgenticMCP-->>MCPClient: OK + MCPClient-->>ExgenticAdapter: closed + ExgenticAdapter-->>Runner: done + + Runner->>OTEL: Record metrics & end span +``` + +## Component Interaction Flow + +```mermaid +flowchart LR + A[Start] --> B[Load Config] + B --> C[Initialize Components] + C --> D{More Tasks?} + D -->|Yes| E[Create Session] + E --> F[Get Task & Session ID] + F --> G[Build Prompt with Session ID] + G --> H[Send to Agent via A2A] + H --> I[Wait for Response] + I --> J[Evaluate Session] + J --> K[Close Session] + K --> L[Record Statistics] + L --> D + D -->|No| M[Print Summary] + M --> N[Shutdown OTEL] + N --> O[End] + + style E fill:#ffe1e1 + style J fill:#e1ffe1 + style K fill:#e1e1ff +``` + +## Data Flow + +```mermaid +graph LR + subgraph "Input" + ENV[Environment Variables] + end + + subgraph "Processing" + CONFIG[Configuration] + SESSION[Session Data] + PROMPT[Formatted Prompt] + RESPONSE[Agent Response] + EVAL[Evaluation Result] + end + + subgraph "Output" + STATS[Statistics] + TELEMETRY[Telemetry Data] + SUMMARY[Console Summary] + end + + ENV --> CONFIG + CONFIG --> SESSION + SESSION --> PROMPT + PROMPT --> RESPONSE + RESPONSE --> EVAL + EVAL --> STATS + STATS --> SUMMARY + STATS --> TELEMETRY +``` + +## Key Design Decisions + +### 1. MCP Client Implementation +- **Decision**: Use official MCP Python SDK +- **Rationale**: Avoid reinventing the wheel, leverage maintained library +- **Trade-off**: Additional dependency vs. implementation effort + +### 2. Session Lifecycle +- **Decision**: Explicit create → evaluate → close pattern +- **Rationale**: Matches Exgentic MCP server design, clear resource management +- **Trade-off**: More API calls vs. cleaner separation of concerns + +### 3. Sequential Execution +- **Decision**: Process one session at a time (MVP) +- **Rationale**: Simpler implementation, easier debugging, matches appworld_a2a_runner +- **Future**: Can add parallel execution later + +### 4. Prompt Format +- **Decision**: Include session_id explicitly in prompt +- **Rationale**: Agent needs to know which session to use for tool calls +- **Format**: Clear instruction to use session_id in all interactions + +### 5. Configuration +- **Decision**: Environment variables for all configuration +- **Rationale**: Matches appworld_a2a_runner pattern, easy deployment +- **Trade-off**: No config file support vs. simplicity + +## Error Handling Strategy + +```mermaid +graph TD + A[Operation] --> B{Success?} + B -->|Yes| C[Continue] + B -->|No| D{Retry?} + D -->|Yes| E[Retry with Backoff] + E --> A + D -->|No| F[Log Error] + F --> G[Record Failure Metric] + G --> H{Abort on Failure?} + H -->|Yes| I[Exit] + H -->|No| J[Continue Next Task] +``` + +## Telemetry Strategy + +### Spans Hierarchy +``` +exgentic_a2a.session +├── exgentic_a2a.mcp.create_session +├── exgentic_a2a.prompt.build +├── exgentic_a2a.a2a.send_prompt +│ └── HTTP spans (auto-instrumented) +├── exgentic_a2a.mcp.evaluate_session +└── exgentic_a2a.mcp.close_session +``` + +### Metrics +- **Counters**: sessions_total, errors_total +- **Histograms**: session_latency_ms, evaluation_latency_ms, creation_latency_ms +- **Gauges**: inflight_sessions + +### Attributes +- `exgentic.session_id` +- `exgentic.mcp_server_url` +- `exgentic.evaluation_result` +- `a2a.base_url` +- `task.status` \ No newline at end of file diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..7c94ec2 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,284 @@ +# Exgentic A2A Runner - Implementation Summary + +## Project Overview + +The `exgentic_a2a_runner` is a test harness that integrates Exgentic benchmarks with Kagenti agents using the A2A protocol. It follows the execution model defined in [GitHub Issue #963](https://github.com/kagenti/kagenti/issues/963). + +## What We're Building + +A standalone Python runner that: +1. Connects to an Exgentic MCP server to get benchmark tasks +2. Creates isolated sessions for each task +3. Sends tasks to Kagenti agents via A2A protocol +4. Evaluates agent performance using the MCP server +5. Collects comprehensive telemetry data +6. Provides detailed statistics and reporting + +## Key Design Principles + +### 1. **Modular Architecture** +- Clear separation of concerns (MCP, A2A, OTEL, Config) +- Reusable components from `appworld_a2a_runner` +- Easy to test and maintain + +### 2. **Sequential Execution (MVP)** +- One session at a time for simplicity +- Easier debugging and monitoring +- Can be extended to parallel execution later + +### 3. **Explicit Session Lifecycle** +``` +CREATE → USE → EVALUATE → CLOSE +``` + +### 4. **Configuration-Driven** +- All settings via environment variables +- No code changes needed for different deployments +- Easy integration with CI/CD + +### 5. **Comprehensive Observability** +- OpenTelemetry for traces, metrics, and logs +- Detailed session-level tracking +- Performance analytics + +## File Structure + +``` +exgentic_a2a_runner/ +├── pyproject.toml # Dependencies: mcp, requests, opentelemetry +├── README.md # User documentation +├── example.env # Configuration template +├── .gitignore # Git ignore patterns +└── exgentic_a2a_runner/ + ├── __init__.py # Package init + ├── config.py # Config classes (ExgenticConfig, A2AConfig, etc.) + ├── mcp_client.py # MCP SDK wrapper + ├── exgentic_adapter.py # Session lifecycle management + ├── a2a_client.py # A2A protocol client (from appworld) + ├── prompt.py # Prompt builder with session_id + ├── otel.py # OpenTelemetry instrumentation + └── runner.py # Main orchestration logic +``` + +## Core Components + +### 1. MCPClient (`mcp_client.py`) +**Purpose**: Communicate with Exgentic MCP server using official SDK + +**Key Methods**: +- `initialize()`: Connect to MCP server +- `call_tool(name, arguments)`: Generic tool invocation +- `create_session()`: Create new benchmark session +- `evaluate_session(session_id)`: Evaluate session success +- `close_session(session_id)`: Cleanup session +- `shutdown()`: Close connection + +### 2. ExgenticAdapter (`exgentic_adapter.py`) +**Purpose**: High-level interface to Exgentic operations + +**Key Methods**: +- `initialize()`: Setup MCP client +- `create_session() -> SessionData`: Create and return session info +- `evaluate_session(session_id) -> bool`: Get evaluation result +- `close_session(session_id)`: Close session +- `iterate_sessions()`: Iterator for sequential processing + +**SessionData Class**: +```python +@dataclass +class SessionData: + session_id: str + task: str + created_at: float +``` + +### 3. Prompt Builder (`prompt.py`) +**Purpose**: Format tasks with session_id for agent + +**Format**: +``` +The task you are to complete is: +{task} + +IMPORTANT: Use session id "{session_id}" in all your interactions with the benchmark tools. +``` + +### 4. Runner (`runner.py`) +**Purpose**: Main orchestration following execution model + +**Process Flow**: +```python +for session_data in exgentic_adapter.iterate_sessions(): + start_time = time.time() + + # Build prompt with session_id + prompt = build_prompt(session_data.task, session_data.session_id) + + # Send to agent + response = a2a_client.send_prompt(prompt) + + # Evaluate + success = exgentic_adapter.evaluate_session(session_data.session_id) + + # Close + exgentic_adapter.close_session(session_data.session_id) + + # Record stats + completion_time = time.time() - start_time + stats[session_id] = (completion_time, success) +``` + +## Configuration + +### Required Environment Variables +```bash +EXGENTIC_MCP_SERVER_URL=http://localhost:3000 # MCP server endpoint +A2A_BASE_URL=http://localhost:8000 # Kagenti agent endpoint +``` + +### Optional Environment Variables +```bash +# Exgentic Configuration +EXGENTIC_MCP_TIMEOUT_SECONDS=60 +MAX_TASKS=10 +ABORT_ON_FAILURE=false + +# A2A Configuration +A2A_TIMEOUT_SECONDS=300 +A2A_AUTH_TOKEN= +A2A_VERIFY_TLS=true +A2A_ENDPOINT_PATH=/v1/chat + +# OpenTelemetry +OTEL_SERVICE_NAME=exgentic-a2a-runner +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +OTEL_EXPORTER_OTLP_PROTOCOL=grpc + +# Debug +LOG_PROMPT=0 +LOG_RESPONSE=0 +``` + +## Dependencies + +```toml +dependencies = [ + "mcp>=0.9.0", # MCP protocol + "requests>=2.28.0", # HTTP client + "opentelemetry-api>=1.20.0", # OTEL API + "opentelemetry-sdk>=1.20.0", # OTEL SDK + "opentelemetry-exporter-otlp>=1.20.0", # OTEL exporter + "opentelemetry-instrumentation-requests>=0.41b0", # HTTP instrumentation +] +``` + +## Telemetry + +### Traces +- `exgentic_a2a.session`: Overall session processing +- `exgentic_a2a.mcp.create_session`: Session creation +- `exgentic_a2a.prompt.build`: Prompt construction +- `exgentic_a2a.a2a.send_prompt`: Agent invocation +- `exgentic_a2a.mcp.evaluate_session`: Evaluation +- `exgentic_a2a.mcp.close_session`: Cleanup + +### Metrics +- `exgentic_a2a_sessions_total{status=success|failed}` +- `exgentic_a2a_session_latency_ms` +- `exgentic_a2a_evaluation_latency_ms` +- `exgentic_a2a_session_creation_latency_ms` +- `exgentic_a2a_inflight_sessions` + +### Attributes +- `exgentic.session_id` +- `exgentic.mcp_server_url` +- `exgentic.evaluation_result` +- `a2a.base_url` +- `task.status` + +## Usage + +```bash +# Install +cd exgentic_a2a_runner +uv sync --python 3.12 +source .venv/bin/activate + +# Configure +cp example.env .env +# Edit .env with your settings + +# Run +uv run exgentic-a2a-runner +``` + +## Output + +### Console Summary +``` +============================================================ +RUN SUMMARY +============================================================ +Sessions Attempted: 100 +Sessions Succeeded: 95 +Sessions Failed: 5 +Total Wall Time: 1234.56s +Average Latency: 12345.67ms +P50 Latency: 10000.00ms +P95 Latency: 20000.00ms +Success Rate: 95.00% +============================================================ +``` + +## Differences from AppWorld Runner + +| Aspect | AppWorld Runner | Exgentic Runner | +|--------|----------------|-----------------| +| **Task Source** | AppWorld dataset enumeration | MCP server `create_session` | +| **Protocol** | Direct AppWorld API | MCP protocol | +| **Session Management** | Implicit (AppWorld context) | Explicit (create/close) | +| **Evaluation** | AppWorld evaluation system | MCP `evaluate_session` | +| **Prompt Format** | Task + supervisor + apps | Task + session_id | +| **Dependencies** | `appworld` package | `mcp` package | + +## Implementation Checklist + +- [x] Architecture design +- [x] Component specifications +- [x] Configuration design +- [x] Telemetry design +- [ ] Create directory structure +- [ ] Implement MCPClient +- [ ] Implement ExgenticAdapter +- [ ] Implement prompt builder +- [ ] Implement Runner +- [ ] Reuse/adapt A2A client +- [ ] Add OTEL instrumentation +- [ ] Create configuration files +- [ ] Write README +- [ ] Test with real services + +## Next Steps + +1. **Review & Approve Plan**: Ensure design meets requirements +2. **Switch to Code Mode**: Begin implementation +3. **Implement Core Components**: MCPClient, ExgenticAdapter, Runner +4. **Add Telemetry**: OTEL instrumentation +5. **Create Documentation**: README, examples +6. **Test Integration**: With real Exgentic MCP server and Kagenti agent +7. **Iterate**: Based on testing feedback + +## Success Criteria + +✅ Sequential execution of benchmark tasks via MCP server +✅ Proper session lifecycle management (create → evaluate → close) +✅ Integration with Kagenti agents via A2A protocol +✅ Session_id included in prompts for agent use +✅ Comprehensive OpenTelemetry instrumentation +✅ Configuration via environment variables +✅ Summary statistics and reporting +✅ Proper error handling and logging + +## Questions? + +If you have any questions or need clarification on any aspect of the design, please ask before we proceed to implementation! \ No newline at end of file diff --git a/exgentic_a2a_runner/.gitignore b/exgentic_a2a_runner/.gitignore new file mode 100644 index 0000000..017fe58 --- /dev/null +++ b/exgentic_a2a_runner/.gitignore @@ -0,0 +1,47 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Environment +.env +.env.local + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/exgentic_a2a_runner/IMPLEMENTATION_CHECKLIST.md b/exgentic_a2a_runner/IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 0000000..b885dde --- /dev/null +++ b/exgentic_a2a_runner/IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,208 @@ +# Exgentic A2A Runner - Implementation Checklist + +## ✅ Completed Components + +### Core Structure +- [x] Directory structure created (`exgentic_a2a_runner/exgentic_a2a_runner/`) +- [x] Package initialization (`__init__.py`) +- [x] Project configuration (`pyproject.toml`) +- [x] Environment configuration (`example.env`) +- [x] Git ignore patterns (`.gitignore`) +- [x] Comprehensive documentation (`README.md`) + +### Configuration Management (`config.py`) +- [x] `ExgenticConfig` class with MCP server settings +- [x] `A2AConfig` class for agent endpoint settings +- [x] `OTELConfig` class for telemetry settings +- [x] `DebugConfig` class for logging settings +- [x] `Config` class aggregating all configurations +- [x] Environment variable parsing with defaults +- [x] Validation of required settings + +### MCP Integration (`mcp_client.py`) +- [x] `MCPClient` class using official MCP SDK +- [x] Async initialization with MCP server +- [x] `create_session()` method returning (session_id, task) +- [x] `evaluate_session(session_id)` method returning success boolean +- [x] `close_session(session_id)` method for cleanup +- [x] Generic `call_tool()` method for MCP operations +- [x] Proper error handling and logging +- [x] Graceful shutdown + +### Exgentic Adapter (`exgentic_adapter.py`) +- [x] `ExgenticAdapter` class wrapping MCP client +- [x] `SessionData` dataclass for session information +- [x] Synchronous wrapper around async MCP operations +- [x] `create_session()` returning SessionData +- [x] `evaluate_session()` for session evaluation +- [x] `close_session()` for cleanup +- [x] `iterate_sessions()` generator for sequential processing +- [x] Session counter and max_tasks limit support +- [x] Proper initialization and shutdown + +### A2A Client (`a2a_client.py`) +- [x] Copied from `appworld_a2a_runner` +- [x] `A2AProxyClient` class for agent communication +- [x] Agent card discovery +- [x] JSON-RPC protocol implementation +- [x] Message sending and task polling +- [x] Response extraction from messages and tasks +- [x] Timeout and error handling + +### Prompt Builder (`prompt.py`) +- [x] `build_prompt()` function +- [x] Includes task description +- [x] Includes session_id with clear instructions +- [x] Emphasizes importance of using session_id in tool calls + +### OpenTelemetry Instrumentation (`otel.py`) +- [x] `OTELInstrumentation` class +- [x] Trace provider initialization +- [x] Metric provider initialization +- [x] `session_span()` context manager for session tracking +- [x] `child_span()` for nested operations +- [x] Session-specific metrics: + - [x] `exgentic_a2a_sessions_total` counter + - [x] `exgentic_a2a_session_latency_ms` histogram + - [x] `exgentic_a2a_evaluation_latency_ms` histogram + - [x] `exgentic_a2a_session_creation_latency_ms` histogram + - [x] `exgentic_a2a_inflight_sessions` gauge +- [x] Reused metrics from appworld: + - [x] `exgentic_a2a_a2a_latency_ms` histogram + - [x] `exgentic_a2a_prompt_size_chars` histogram + - [x] `exgentic_a2a_response_size_chars` histogram + - [x] `exgentic_a2a_errors_total` counter +- [x] Span attributes for session tracking +- [x] `record_success()` with evaluation result +- [x] `record_failure()` with error details +- [x] `record_evaluation()` for evaluation metrics +- [x] `record_session_creation()` for creation metrics +- [x] HTTP request auto-instrumentation + +### Main Runner (`runner.py`) +- [x] `Runner` class orchestrating execution +- [x] `SessionResult` dataclass for results +- [x] `RunSummary` class for statistics +- [x] Execution model implementation: + - [x] Create session via MCP + - [x] Build prompt with session_id + - [x] Send to agent via A2A + - [x] Evaluate session via MCP + - [x] Close session via MCP + - [x] Record statistics +- [x] `process_session()` method following exact flow +- [x] Sequential session iteration +- [x] Comprehensive error handling +- [x] Graceful shutdown on errors +- [x] Session cleanup even on failure +- [x] OTEL span creation and tracking +- [x] Summary statistics calculation +- [x] Console output formatting +- [x] Command-line argument parsing +- [x] Verbose logging support +- [x] Exit code handling + +## ✅ Documentation +- [x] Comprehensive README.md with: + - [x] Feature list + - [x] Architecture description + - [x] Installation instructions + - [x] Configuration reference + - [x] Usage examples + - [x] Output format documentation + - [x] OpenTelemetry metrics/traces documentation + - [x] Comparison with appworld_a2a_runner + - [x] Execution flow diagram + - [x] Troubleshooting guide +- [x] Example environment file with all variables +- [x] Planning documents (EXGENTIC_A2A_RUNNER_PLAN.md) +- [x] Architecture diagrams (EXGENTIC_ARCHITECTURE.md) +- [x] Implementation summary (IMPLEMENTATION_SUMMARY.md) + +## ✅ Alignment with GitHub Issue #963 + +### Required Features +- [x] MCP server integration for benchmark access +- [x] Admin tools support: `create_session`, `evaluate_session`, `close_session` +- [x] Session-based execution model +- [x] A2A protocol for agent communication +- [x] Session_id included in prompts +- [x] Sequential execution (MVP requirement) +- [x] Statistics collection and reporting +- [x] OpenTelemetry instrumentation + +### Execution Model Match +``` +✅ (session_id, task) = benchmark_mcp.call("create_session") +✅ startTime = time() +✅ agent.invoke_agent("{task}. Use session id {session_id}") +✅ completionTime = time() - startTime +✅ success = benchmark_mcp.call("evaluate_session", {"session_id": session_id}) +✅ stats[session_id] = (completion_time, success) +✅ benchmark_mcp.call("close_session", {"session_id": session_id}) +``` + +## 📋 Dependencies + +### Python Packages (in pyproject.toml) +- [x] `mcp>=0.9.0` - Official MCP SDK +- [x] `requests>=2.28.0` - HTTP client +- [x] `opentelemetry-api>=1.20.0` - OTEL API +- [x] `opentelemetry-sdk>=1.20.0` - OTEL SDK +- [x] `opentelemetry-exporter-otlp>=1.20.0` - OTEL exporter +- [x] `opentelemetry-instrumentation-requests>=0.41b0` - HTTP instrumentation + +### Dev Dependencies +- [x] `pytest>=7.0.0` +- [x] `pytest-mock>=3.10.0` +- [x] `black>=23.0.0` +- [x] `mypy>=1.0.0` + +## 🎯 Key Design Decisions + +1. **Sequential Execution**: ✅ Implemented for MVP simplicity +2. **Official MCP SDK**: ✅ Used for reliable protocol communication +3. **Explicit Session Lifecycle**: ✅ Create → Use → Evaluate → Close +4. **Session-aware Prompts**: ✅ Session_id prominently included +5. **Comprehensive Telemetry**: ✅ Traces, metrics, and logs +6. **Environment Configuration**: ✅ All settings via env vars +7. **Error Handling**: ✅ Graceful degradation and cleanup +8. **Reusable Components**: ✅ A2A client and OTEL base from appworld + +## 🔍 Code Quality + +- [x] Consistent naming conventions +- [x] Comprehensive docstrings +- [x] Type hints where applicable +- [x] Logging at appropriate levels +- [x] Error messages with context +- [x] Clean separation of concerns +- [x] Follows appworld_a2a_runner patterns + +## 📊 Testing Readiness + +The implementation is ready for testing with: +- Real Exgentic MCP server +- Kagenti generalist agent +- OTLP collector for telemetry + +### To Test: +1. Set up Exgentic MCP server +2. Deploy Kagenti agent with A2A endpoint +3. Configure environment variables +4. Run: `uv run exgentic-a2a-runner` +5. Verify session creation, execution, evaluation, and cleanup +6. Check telemetry data in OTLP collector + +## ✨ Summary + +All components have been successfully implemented following the requirements from GitHub Issue #963. The harness: + +- ✅ Integrates with Exgentic MCP server +- ✅ Communicates with Kagenti agents via A2A +- ✅ Implements the exact execution model specified +- ✅ Provides comprehensive observability +- ✅ Follows the appworld_a2a_runner pattern +- ✅ Is fully documented and ready for deployment + +The implementation is **COMPLETE** and ready for integration testing with actual Exgentic MCP server and Kagenti agents. \ No newline at end of file diff --git a/exgentic_a2a_runner/QUICKSTART.md b/exgentic_a2a_runner/QUICKSTART.md new file mode 100644 index 0000000..5ba306e --- /dev/null +++ b/exgentic_a2a_runner/QUICKSTART.md @@ -0,0 +1,146 @@ +# Quick Start Guide - Running Against Kagenti Cluster + +## Prerequisites + +✅ kubectl configured with `kind-kagenti` context +✅ Services running in team1 namespace: +- `exgentic-mcp-tau2-mcp` on port 8000 +- `generic-agent2` on port 8080 + +## Quick Run + +The easiest way to run the harness is using the provided script: + +```bash +cd exgentic_a2a_runner +./run-with-port-forward.sh +``` + +This script will: +1. Set up port forwarding to both services +2. Test connectivity +3. Run the harness with verbose logging +4. Clean up port forwards on exit + +## Manual Run + +If you prefer to set up port forwarding manually: + +### Terminal 1: Port Forward MCP Server +```bash +kubectl port-forward -n team1 svc/exgentic-mcp-tau2-mcp 8000:8000 +``` + +### Terminal 2: Port Forward A2A Agent +```bash +kubectl port-forward -n team1 svc/generic-agent2 8080:8080 +``` + +### Terminal 3: Run Harness +```bash +cd exgentic_a2a_runner +source .venv/bin/activate +uv run exgentic-a2a-runner --verbose +``` + +## Configuration + +The `.env` file is already configured for the Kagenti cluster: + +```bash +EXGENTIC_MCP_SERVER_URL=http://localhost:8000 +A2A_BASE_URL=http://localhost:8080 +MAX_TASKS=3 # Start with 3 sessions for testing +LOG_PROMPT=1 # Enable prompt logging +LOG_RESPONSE=1 # Enable response logging +``` + +## What to Expect + +The harness will: +1. Connect to the MCP server and create a session +2. Get a task from the Tau-Bench benchmark +3. Build a prompt with the session_id +4. Send the prompt to the generic-agent2 via A2A +5. Wait for the agent to complete the task +6. Evaluate the session via MCP +7. Close the session +8. Print statistics + +### Expected Output + +``` +2026-03-17 18:52:00 - INFO - Initializing runner components +2026-03-17 18:52:00 - INFO - Initializing OpenTelemetry instrumentation +2026-03-17 18:52:00 - INFO - Initializing Exgentic adapter +2026-03-17 18:52:01 - INFO - Creating new session +2026-03-17 18:52:01 - INFO - Processing session: session_abc123 +2026-03-17 18:52:05 - INFO - Session session_abc123 completed in 4523.45ms (evaluation: success) +... +============================================================ +RUN SUMMARY +============================================================ +Sessions Attempted: 3 +Sessions Succeeded: 3 +Sessions Failed: 0 +Evaluation Success: 100.0% +Total Wall Time: 15.23s +Average Latency: 5076.67ms +P50 Latency: 5000.00ms +P95 Latency: 5500.00ms +============================================================ +``` + +## Troubleshooting + +### Port Forward Issues + +If port forwarding fails: +```bash +# Kill existing port forwards +pkill -f "port-forward" + +# Check if ports are in use +lsof -i :8000 +lsof -i :8080 + +# Restart port forwards +kubectl port-forward -n team1 svc/exgentic-mcp-tau2-mcp 8000:8000 & +kubectl port-forward -n team1 svc/generic-agent2 8080:8080 & +``` + +### MCP Connection Issues + +If you see "MCP client initialization failed": +- Verify the MCP server is running: `kubectl get pods -n team1 | grep exgentic` +- Check MCP server logs: `kubectl logs -n team1 -l app=exgentic-mcp-tau2-mcp` +- Test connectivity: `curl http://localhost:8000/health` (if health endpoint exists) + +### A2A Connection Issues + +If you see "A2A request failed": +- Verify the agent is running: `kubectl get pods -n team1 | grep generic-agent2` +- Check agent logs: `kubectl logs -n team1 -l app=generic-agent2` +- Test agent card: `curl http://localhost:8080/.well-known/agent-card.json` + +### Session Evaluation Failures + +If sessions complete but evaluation fails: +- Check if the agent is using the session_id correctly in its tool calls +- Review agent logs to see what tools it's calling +- Verify the agent has access to the MCP server for tool execution + +## Next Steps + +After successful test run: +1. Increase `MAX_TASKS` in `.env` for longer runs +2. Enable OTLP exporter for telemetry collection +3. Run with different benchmarks by changing the MCP server +4. Analyze results and agent performance + +## Support + +For issues: +- Check logs with `--verbose` flag +- Review agent and MCP server logs in Kubernetes +- See main README.md for detailed documentation \ No newline at end of file diff --git a/exgentic_a2a_runner/README.md b/exgentic_a2a_runner/README.md new file mode 100644 index 0000000..ca910b8 --- /dev/null +++ b/exgentic_a2a_runner/README.md @@ -0,0 +1,270 @@ +# Exgentic A2A Runner + +A standalone Python runner that integrates Exgentic benchmarks with Kagenti agents using the A2A (Agent-to-Agent) protocol. This harness implements the execution model defined in [GitHub Issue #963](https://github.com/kagenti/kagenti/issues/963). + +## Features + +- **Exgentic MCP Integration**: Communicates with Exgentic MCP server for benchmark tasks +- **Sequential session processing**: One session at a time for simplicity and reliability +- **A2A protocol support**: Communicates with remote agents using the A2A protocol via JSON-RPC over HTTP +- **Session lifecycle management**: Explicit create → use → evaluate → close pattern +- **OpenTelemetry instrumentation**: Comprehensive traces, metrics, and logs +- **Strict failure handling**: Any error or timeout marks the session as failed +- **Configurable via environment variables**: Easy deployment and configuration +- **Official MCP SDK**: Uses the MCP Python SDK for reliable protocol communication + +## Architecture + +The runner follows this execution model for each benchmark session: + +1. **Create Session**: `(session_id, task) = mcp_server.create_session()` +2. **Build Prompt**: Include session_id in task instructions +3. **Invoke Agent**: `agent.invoke_agent("{task}. Use session id {session_id}")` +4. **Evaluate Session**: `success = mcp_server.evaluate_session(session_id)` +5. **Close Session**: `mcp_server.close_session(session_id)` +6. **Record Statistics**: Track completion time and success rate + +## Installation + +### Prerequisites + +- Python 3.11 or 3.12 (Python 3.13 is **not supported** due to dependency compatibility) +- [uv](https://docs.astral.sh/uv/) package manager +- Access to an Exgentic MCP server +- Access to an A2A-compatible agent endpoint (e.g., Kagenti generalist agent) + +### Install from source + +```bash +git clone git@github.com:kagenti/workload-harness.git +cd workload-harness/exgentic_a2a_runner +uv sync --python 3.12 +source .venv/bin/activate +``` + +## Configuration + +```bash +cp example.env .env +``` + +Configure the .env file as needed. + +### Required Variables + +| Environment Variable | Default Setting | Required? | Description | +| --- | --- | --- | --- | +| `EXGENTIC_MCP_SERVER_URL` | `(none)` | Yes | URL for the Exgentic MCP server that provides benchmark tasks. | +| `A2A_BASE_URL` | `(none)` | Yes | Base URL for the target agent to run the tests against. Must be A2A compatible. | + +### Optional Variables + +| Environment Variable | Default Setting | Required? | Description | +| --- | --- | --- | --- | +| `EXGENTIC_MCP_TIMEOUT_SECONDS` | `60` | No | Timeout for MCP operations in seconds. | +| `MAX_TASKS` | `(none)` | No | Maximum number of sessions to process before exiting. | +| `ABORT_ON_FAILURE` | `false` | No | Stops processing after the first failed session when enabled. | +| `A2A_TIMEOUT_SECONDS` | `300` | No | Timeout for each A2A request in seconds. | +| `A2A_AUTH_TOKEN` | `(none)` | No | Bearer token sent for A2A endpoint authentication. | +| `A2A_VERIFY_TLS` | `true` | No | Whether TLS certificates are verified for HTTPS requests. | +| `A2A_ENDPOINT_PATH` | `/v1/chat` | No | Endpoint path appended to `A2A_BASE_URL` for requests. | +| `OTEL_SERVICE_NAME` | `exgentic-a2a-runner` | No | OpenTelemetry service name reported with traces. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `(none)` | No | OTLP collector endpoint used to export telemetry. | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | No | OTLP transport protocol (`grpc` or `http/protobuf`). | +| `OTEL_RESOURCE_ATTRIBUTES` | `(none)` | No | Additional OpenTelemetry resource attributes (`key=value`). | +| `OTEL_INSTRUMENT_REQUESTS` | `true` | No | Enables automatic instrumentation for HTTP requests. | +| `OTEL_EXPORTER_OTLP_INSECURE` | `true` | No | Use insecure connection for OTLP exporter. | +| `LOG_PROMPT` | `0` | No | Enables logging of prompt payloads for debugging. | +| `LOG_RESPONSE` | `0` | No | Enables logging of response payloads for debugging. | + +## Usage + +### Basic Usage + +```bash +uv run exgentic-a2a-runner +``` + +### With Verbose Logging + +```bash +uv run exgentic-a2a-runner --verbose +``` + +## Output + +### Console Summary + +At the end of each run, a summary is printed: + +``` +============================================================ +RUN SUMMARY +============================================================ +Sessions Attempted: 100 +Sessions Succeeded: 95 +Sessions Failed: 5 +Evaluation Success: 92.6% +Total Wall Time: 1234.56s +Average Latency: 12345.67ms +P50 Latency: 10000.00ms +P95 Latency: 20000.00ms +============================================================ +``` + +### OpenTelemetry Data + +The runner emits comprehensive telemetry: + +#### Traces + +Each session creates a span (`exgentic_a2a.session`) with: + +**Attributes:** +- `exgentic.session_id`: Session identifier +- `exgentic.mcp_server_url`: MCP server URL +- `exgentic.evaluation_result`: Whether evaluation was successful +- `a2a.base_url`: A2A endpoint URL +- `a2a.timeout_seconds`: Timeout value +- `prompt.chars`: Prompt size in characters +- `response.chars`: Response size in characters +- `session.status`: `success` or `failed` +- `a2a.duration_ms`: End-to-end A2A operation latency in milliseconds + +**Child spans:** +- `exgentic_a2a.prompt.build`: Prompt construction +- `exgentic_a2a.a2a.send_prompt`: End-to-end A2A `send_prompt` call +- `exgentic_a2a.mcp.evaluate_session`: Session evaluation +- `exgentic_a2a.mcp.close_session`: Session cleanup + +**Auto-instrumented HTTP spans:** +- Outbound `requests` spans for agent-card discovery, `message/send`, and `tasks/get` calls + +**Events:** +- `prompt_built`: When prompt is constructed +- `session_failed`: When session fails (includes error details) + +#### Metrics + +**Counters:** +- `exgentic_a2a_sessions_total{status=success|failed}`: Total sessions processed +- `exgentic_a2a_errors_total{error_type=...}`: Total errors by type + +**Histograms:** +- `exgentic_a2a_session_latency_ms`: End-to-end session latency +- `exgentic_a2a_evaluation_latency_ms`: Evaluation operation latency +- `exgentic_a2a_session_creation_latency_ms`: Session creation latency +- `exgentic_a2a_a2a_latency_ms`: A2A request latency +- `exgentic_a2a_prompt_size_chars`: Prompt size distribution +- `exgentic_a2a_response_size_chars`: Response size distribution + +**Gauge:** +- `exgentic_a2a_inflight_sessions`: Current sessions in flight (0 or 1) + +## Key Differences from AppWorld Runner + +| Aspect | AppWorld Runner | Exgentic Runner | +|--------|----------------|-----------------| +| **Task Source** | AppWorld dataset enumeration | MCP server `create_session` | +| **Protocol** | Direct AppWorld API | MCP protocol | +| **Session Management** | Implicit (AppWorld context) | Explicit (create/evaluate/close) | +| **Evaluation** | AppWorld evaluation system | MCP `evaluate_session` | +| **Prompt Format** | Task + supervisor + apps | Task + session_id | +| **Dependencies** | `appworld` package | `mcp` package | + +## Execution Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ For Each Session │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. Create Session │ +│ └─> MCP: create_session() → (session_id, task) │ +│ │ +│ 2. Build Prompt │ +│ └─> Include session_id in instructions │ +│ │ +│ 3. Invoke Agent │ +│ └─> A2A: send_prompt(prompt) → response │ +│ │ +│ 4. Evaluate Session │ +│ └─> MCP: evaluate_session(session_id) → success │ +│ │ +│ 5. Close Session │ +│ └─> MCP: close_session(session_id) │ +│ │ +│ 6. Record Statistics │ +│ └─> Track time, success, evaluation result │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Current Limitations + +- Sequential execution only (no concurrency) +- No retry mechanism for failed operations +- No streaming response support +- Assumes MCP server is already configured for specific benchmark + +## Troubleshooting + +### MCP Connection Issues + +If you see errors connecting to the MCP server: +- Verify `EXGENTIC_MCP_SERVER_URL` is correct +- Check that the MCP server is running and accessible +- Ensure the MCP server supports the required tools: `create_session`, `evaluate_session`, `close_session` + +### A2A Communication Issues + +If the agent doesn't respond or times out: +- Verify `A2A_BASE_URL` is correct +- Check `A2A_TIMEOUT_SECONDS` is sufficient for your tasks +- Ensure the agent is A2A-compatible and running +- Check if `A2A_AUTH_TOKEN` is required and set correctly + +### Session Evaluation Failures + +If sessions complete but evaluation fails: +- Check agent logs to see if it's using the session_id correctly +- Verify the agent has access to the benchmark tools via MCP +- Ensure the agent is calling tools with the correct session_id parameter + +## Development + +### Running Tests + +```bash +uv run pytest +``` + +### Code Formatting + +```bash +uv run black exgentic_a2a_runner/ +``` + +### Type Checking + +```bash +uv run mypy exgentic_a2a_runner/ +``` + +## Contributing + +Contributions are welcome! Please ensure: +- Code follows the existing style +- Tests pass +- Documentation is updated +- Commit messages are clear + +## License + +See LICENSE file in the repository root. + +## Support + +For issues and questions: +- GitHub Issues: https://github.com/kagenti/workload-harness/issues +- Related Issue: https://github.com/kagenti/kagenti/issues/963 \ No newline at end of file diff --git a/exgentic_a2a_runner/example.env b/exgentic_a2a_runner/example.env new file mode 100644 index 0000000..25cd0db --- /dev/null +++ b/exgentic_a2a_runner/example.env @@ -0,0 +1,78 @@ +# Example Environment Configuration for Exgentic A2A Runner +# Copy this file to .env and customize for your environment + +# ============================================================ +# REQUIRED CONFIGURATION +# ============================================================ + +# Exgentic MCP Server URL (REQUIRED) +# The endpoint for the Exgentic MCP server that provides benchmark tasks +EXGENTIC_MCP_SERVER_URL=http://localhost:3000 + +# A2A endpoint base URL (REQUIRED) +# The Kagenti agent endpoint that will execute the tasks +A2A_BASE_URL=http://localhost:8000 + +# ============================================================ +# EXGENTIC CONFIGURATION (Optional) +# ============================================================ + +# MCP operation timeout in seconds (default: 60) +EXGENTIC_MCP_TIMEOUT_SECONDS=60 + +# Maximum number of tasks/sessions to process (useful for testing) +MAX_TASKS=10 + +# Stop processing on first failure (default: false) +ABORT_ON_FAILURE=false + +# ============================================================ +# A2A CONFIGURATION (Optional) +# ============================================================ + +# Request timeout in seconds (default: 300) +A2A_TIMEOUT_SECONDS=300 + +# Bearer token for authentication (if required by your endpoint) +# A2A_AUTH_TOKEN=your-secret-token-here + +# Verify TLS certificates (default: true) +A2A_VERIFY_TLS=true + +# Custom endpoint path (default: /v1/chat) +# A2A_ENDPOINT_PATH=/v1/chat + +# ============================================================ +# OPENTELEMETRY CONFIGURATION (Optional) +# ============================================================ + +# Service name for telemetry (default: exgentic-a2a-runner) +OTEL_SERVICE_NAME=exgentic-a2a-runner + +# OTLP exporter endpoint (if not set, telemetry won't be exported) +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 + +# OTLP protocol (default: grpc) +# Options: grpc, http/protobuf +OTEL_EXPORTER_OTLP_PROTOCOL=grpc + +# Additional resource attributes (comma-separated key=value pairs) +# OTEL_RESOURCE_ATTRIBUTES=env=development,team=research,version=1.0 + +# Enable requests auto-instrumentation for standardized HTTP client spans (default: true) +OTEL_INSTRUMENT_REQUESTS=true + +# Use insecure connection for OTLP (default: true) +OTEL_EXPORTER_OTLP_INSECURE=true + +# ============================================================ +# DEBUG CONFIGURATION (Optional) +# ============================================================ + +# Log prompt details (default: 0) +# WARNING: May log sensitive information +LOG_PROMPT=0 + +# Log response details (default: 0) +# WARNING: May log sensitive information +LOG_RESPONSE=0 \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py b/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py new file mode 100644 index 0000000..c1f4801 --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py @@ -0,0 +1,9 @@ +"""Exgentic A2A Runner - Benchmark harness for Kagenti agents. + +This package provides a test harness that integrates Exgentic benchmarks +with Kagenti agents using the A2A protocol. +""" + +__version__ = "0.1.0" + +# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py new file mode 100644 index 0000000..1563df4 --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py @@ -0,0 +1,334 @@ +"""A2A client for communicating with remote agent endpoints. + +Implements the A2A protocol for sending prompts and receiving responses using JSON-RPC. +""" + +import logging +import time +import uuid +from typing import Any, Dict, Optional +from urllib.parse import urlparse + +import requests + +from .config import A2AConfig + +logger = logging.getLogger(__name__) + + +class A2AProxyClient: + """Client for A2A protocol communication using JSON-RPC.""" + + def __init__(self, config: A2AConfig): + """Initialize A2A client. + + Args: + config: A2A configuration + """ + self.config = config + self.session = requests.Session() + + # Set default headers + self.session.headers.update( + { + "Content-Type": "application/json", + "Accept": "application/json", + } + ) + + # Set auth token if provided + if config.auth_token: + self.session.headers["Authorization"] = f"Bearer {config.auth_token}" + + # Fetch agent card and determine RPC URL + self.rpc_url = self._discover_rpc_url() + logger.info(f"A2A client initialized with RPC URL: {self.rpc_url}") + + def _normalize_endpoint_path(self) -> str: + """Normalize configured endpoint path to '/path' format.""" + endpoint_path = (self.config.endpoint_path or "/").strip() + if not endpoint_path: + endpoint_path = "/" + if not endpoint_path.startswith("/"): + endpoint_path = "/" + endpoint_path + return endpoint_path + + def _build_rpc_url(self, base_url: str) -> str: + """Build RPC URL by appending configured endpoint path to a base URL.""" + return base_url.rstrip("/") + self._normalize_endpoint_path() + + def _get_agent_card(self) -> Dict[str, Any]: + """Fetch the agent card from the standard discovery location. + + Returns: + Agent card as dict + + Raises: + requests.RequestException: On network or HTTP errors + """ + card_url = self.config.base_url.rstrip("/") + "/.well-known/agent-card.json" + logger.debug(f"Fetching agent card from {card_url}") + + try: + response = self.session.get( + card_url, + timeout=30, + verify=self.config.verify_tls, + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.warning(f"Failed to fetch agent card: {e}") + raise + + def _discover_rpc_url(self) -> str: + """Discover the JSON-RPC endpoint URL from the agent card. + + Returns: + JSON-RPC endpoint URL + """ + try: + card = self._get_agent_card() + service_url = card.get("url") + + # If the card has an explicit non-root path, treat it as the RPC URL. + # Otherwise build URL from base + configured endpoint path. + if service_url: + parsed = urlparse(service_url) + if parsed.path and parsed.path != "/": + rpc_url = service_url.rstrip("/") + else: + rpc_url = self._build_rpc_url(service_url) + else: + rpc_url = self._build_rpc_url(self.config.base_url) + + logger.debug(f"Discovered RPC URL from agent card: {rpc_url}") + return rpc_url + except Exception as e: + # Fallback to configured base_url + endpoint path if card fetch fails + logger.warning(f"Could not fetch agent card, using configured endpoint: {e}") + return self._build_rpc_url(self.config.base_url) + + def _jsonrpc_call( + self, + method: str, + params: Dict[str, Any], + request_id: int, + ) -> Any: + """Make a JSON-RPC call to the agent endpoint. + + Args: + method: JSON-RPC method name (e.g., "message/send", "tasks/get") + params: Method parameters + request_id: Request ID for tracking + + Returns: + Result from the JSON-RPC response + + Raises: + RuntimeError: On JSON-RPC error + requests.RequestException: On network or HTTP errors + """ + payload = { + "jsonrpc": "2.0", + "id": request_id, + "method": method, + "params": params, + } + + logger.debug(f"JSON-RPC call: {method} with request_id={request_id}") + + response = self.session.post( + self.rpc_url, + json=payload, + timeout=self.config.timeout_seconds, + verify=self.config.verify_tls, + ) + response.raise_for_status() + + data = response.json() + if "error" in data: + raise RuntimeError(f"JSON-RPC error: {data['error']}") + + if "result" not in data: + raise RuntimeError(f"JSON-RPC response missing 'result' field: {data}") + + return data["result"] + + def _extract_text_from_message(self, message: Dict[str, Any]) -> str: + """Extract text content from a message object. + + Args: + message: Message object with role and parts + + Returns: + Extracted text content + + Raises: + ValueError: If message format is invalid + """ + # Per spec, a Message has role + parts[{kind:"text", text:"..."}] + if "parts" in message: + parts = message["parts"] + if isinstance(parts, list): + text_parts = [] + for part in parts: + if isinstance(part, dict) and part.get("kind") == "text": + text = part.get("text", "") + if text: + text_parts.append(text) + if text_parts: + return "\n".join(text_parts) + + # Fallback: check for direct content field + if "content" in message: + return str(message["content"]) + + raise ValueError("Could not extract text from message") + + def _extract_text_from_task(self, task: Dict[str, Any]) -> str: + """Extract text content from a completed task. + + Args: + task: Task object + + Returns: + Extracted text content + + Raises: + ValueError: If task format is invalid or task failed + """ + status = task.get("status", {}) + state = status.get("state") + + if state == "failed": + error = status.get("error", "Unknown error") + raise ValueError(f"Task failed: {error}") + + if state == "canceled": + raise ValueError("Task was canceled") + + if state == "rejected": + raise ValueError("Task was rejected") + + # Look for artifacts in task (A2A spec) + if "artifacts" in task: + artifacts = task["artifacts"] + if isinstance(artifacts, list) and len(artifacts) > 0: + artifact = artifacts[0] + if isinstance(artifact, dict) and "parts" in artifact: + parts = artifact["parts"] + if isinstance(parts, list): + text_parts = [] + for part in parts: + if isinstance(part, dict) and part.get("kind") == "text": + text = part.get("text", "") + if text: + text_parts.append(text) + if text_parts: + return "\n".join(text_parts) + + # Look for result in task (fallback) + if "result" in task: + result = task["result"] + if isinstance(result, dict): + # Try to extract message from result + if "message" in result: + return self._extract_text_from_message(result["message"]) + # Try direct text extraction + if "text" in result: + return str(result["text"]) + if "content" in result: + return str(result["content"]) + elif isinstance(result, str): + return result + + raise ValueError("Could not extract text from task result") + + def send_prompt( + self, + prompt: str, + poll_interval_s: float = 0.5, + timeout_s: Optional[float] = None, + ) -> str: + """Send prompt to A2A endpoint and get response. + + Args: + prompt: The prompt text to send + poll_interval_s: Polling interval for task status (default: 0.5s) + timeout_s: Timeout for task completion (default: config timeout) + + Returns: + Plain text response from the agent + + Raises: + requests.RequestException: On network or HTTP errors + ValueError: On invalid response format + TimeoutError: If task doesn't complete in time + Exception: On any other error + """ + if timeout_s is None: + timeout_s = float(self.config.timeout_seconds) + + try: + # Build message per A2A spec: role + parts[{kind:"text", text:"..."}] + message = { + "role": "user", + "parts": [{"kind": "text", "text": prompt}], + "messageId": str(uuid.uuid4()), + } + + logger.debug(f"Sending message to {self.rpc_url}") + + # Send message via JSON-RPC + result = self._jsonrpc_call( + "message/send", + params={"message": message, "metadata": {}}, + request_id=1, + ) + + # The server can return either a Message or a Task + if result.get("kind") != "task": + # It's a Message object - extract text directly + logger.debug("Received direct message response") + return self._extract_text_from_message(result) + + # It's a Task - poll for completion + task_id = result["id"] + logger.debug(f"Received task {task_id}, polling for completion") + + deadline = time.time() + timeout_s + request_id = 2 + + while time.time() < deadline: + task = self._jsonrpc_call( + "tasks/get", + params={"id": task_id}, + request_id=request_id, + ) + request_id += 1 + + logger.debug("Task poll response: %s", task) + + state = task.get("status", {}).get("state") + logger.debug(f"Task {task_id} state: {state}") + + if state in {"completed", "failed", "canceled", "rejected"}: + return self._extract_text_from_task(task) + + time.sleep(poll_interval_s) + + raise TimeoutError(f"Task {task_id} did not finish within {timeout_s}s") + + except requests.Timeout as e: + logger.error(f"A2A request timed out: {e}") + raise + except requests.RequestException as e: + logger.error(f"A2A request failed: {type(e).__name__}: {e}") + raise + except Exception as e: + logger.error(f"A2A request failed: {type(e).__name__}: {e}") + raise + + +# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/config.py b/exgentic_a2a_runner/exgentic_a2a_runner/config.py new file mode 100644 index 0000000..b9054eb --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/config.py @@ -0,0 +1,142 @@ +"""Configuration management for Exgentic A2A Runner. + +Configuration is loaded from environment variables with optional CLI overrides. +""" + +import os +from dataclasses import dataclass +from typing import Optional + + +def _get_bool(key: str, default: bool = False) -> bool: + """Get boolean value from environment variable.""" + value = os.getenv(key, "").lower() + if value in ("1", "true", "yes", "on"): + return True + elif value in ("0", "false", "no", "off"): + return False + return default + + +def _get_int(key: str, default: Optional[int] = None) -> Optional[int]: + """Get integer value from environment variable.""" + value = os.getenv(key) + if value is None: + return default + try: + return int(value) + except ValueError: + return default + + +@dataclass +class ExgenticConfig: + """Exgentic MCP server configuration.""" + + mcp_server_url: str + mcp_timeout_seconds: int = 60 + max_tasks: Optional[int] = None + abort_on_failure: bool = False + + @classmethod + def from_env(cls) -> "ExgenticConfig": + """Load Exgentic configuration from environment variables.""" + mcp_server_url = os.getenv("EXGENTIC_MCP_SERVER_URL") + if not mcp_server_url: + raise ValueError("EXGENTIC_MCP_SERVER_URL environment variable is required") + + return cls( + mcp_server_url=mcp_server_url, + mcp_timeout_seconds=_get_int("EXGENTIC_MCP_TIMEOUT_SECONDS", 60) or 60, + max_tasks=_get_int("MAX_TASKS"), + abort_on_failure=_get_bool("ABORT_ON_FAILURE", False), + ) + + +@dataclass +class A2AConfig: + """A2A endpoint configuration.""" + + base_url: str + timeout_seconds: int = 300 + auth_token: Optional[str] = None + verify_tls: bool = True + endpoint_path: str = "/v1/chat" + + @classmethod + def from_env(cls) -> "A2AConfig": + """Load A2A configuration from environment variables.""" + base_url = os.getenv("A2A_BASE_URL") + if not base_url: + raise ValueError("A2A_BASE_URL environment variable is required") + + return cls( + base_url=base_url, + timeout_seconds=_get_int("A2A_TIMEOUT_SECONDS", 300) or 300, + auth_token=os.getenv("A2A_AUTH_TOKEN"), + verify_tls=_get_bool("A2A_VERIFY_TLS", True), + endpoint_path=os.getenv("A2A_ENDPOINT_PATH", "/v1/chat"), + ) + + +@dataclass +class OTELConfig: + """OpenTelemetry configuration.""" + + service_name: str = "exgentic-a2a-runner" + exporter_endpoint: Optional[str] = None + exporter_protocol: str = "grpc" + resource_attributes: Optional[str] = None + instrument_requests: bool = True + exporter_insecure: bool = True + + @classmethod + def from_env(cls) -> "OTELConfig": + """Load OTEL configuration from environment variables.""" + return cls( + service_name=os.getenv("OTEL_SERVICE_NAME", "exgentic-a2a-runner"), + exporter_endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), + exporter_protocol=os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"), + resource_attributes=os.getenv("OTEL_RESOURCE_ATTRIBUTES"), + instrument_requests=_get_bool("OTEL_INSTRUMENT_REQUESTS", True), + exporter_insecure=_get_bool("OTEL_EXPORTER_OTLP_INSECURE", True), + ) + + +@dataclass +class DebugConfig: + """Debug and logging configuration.""" + + log_prompt: bool = False + log_response: bool = False + + @classmethod + def from_env(cls) -> "DebugConfig": + """Load debug configuration from environment variables.""" + return cls( + log_prompt=_get_bool("LOG_PROMPT", False), + log_response=_get_bool("LOG_RESPONSE", False), + ) + + +@dataclass +class Config: + """Complete runner configuration.""" + + exgentic: ExgenticConfig + a2a: A2AConfig + otel: OTELConfig + debug: DebugConfig + + @classmethod + def from_env(cls) -> "Config": + """Load complete configuration from environment variables.""" + return cls( + exgentic=ExgenticConfig.from_env(), + a2a=A2AConfig.from_env(), + otel=OTELConfig.from_env(), + debug=DebugConfig.from_env(), + ) + + +# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py new file mode 100644 index 0000000..1b9e2ce --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py @@ -0,0 +1,164 @@ +"""Exgentic adapter for session management and task execution. + +Provides high-level interface to Exgentic MCP server operations. +""" + +import logging +import time +from dataclasses import dataclass +from typing import Iterator, Optional + +from .config import ExgenticConfig +from .mcp_client import MCPClient + +logger = logging.getLogger(__name__) + + +@dataclass +class SessionData: + """Container for session data from Exgentic.""" + + session_id: str + task: str + created_at: float + + +class ExgenticAdapter: + """Adapter for accessing Exgentic benchmark sessions.""" + + def __init__(self, config: ExgenticConfig): + """Initialize Exgentic adapter. + + Args: + config: Exgentic configuration + """ + self.config = config + self.mcp_client = MCPClient(config) + self._initialized = False + self._session_count = 0 + + def initialize(self) -> None: + """Initialize Exgentic adapter and MCP client.""" + logger.info("Initializing Exgentic adapter") + self.mcp_client.initialize() + self._initialized = True + logger.info("Exgentic adapter initialized successfully") + + def shutdown(self) -> None: + """Shutdown Exgentic adapter and MCP client.""" + if self._initialized: + logger.info("Shutting down Exgentic adapter") + self.mcp_client.shutdown() + self._initialized = False + + def create_session(self) -> SessionData: + """Create a new benchmark session. + + Returns: + SessionData containing session_id and task + + Raises: + RuntimeError: If adapter not initialized or session creation fails + """ + if not self._initialized: + raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") + + logger.info("Creating new session") + created_at = time.time() + + try: + session_id, task = self.mcp_client.create_session() + + self._session_count += 1 + logger.info(f"Created session {self._session_count}: {session_id}") + + return SessionData( + session_id=session_id, + task=task, + created_at=created_at, + ) + + except Exception as e: + logger.error(f"Failed to create session: {e}") + raise + + def evaluate_session(self, session_id: str) -> bool: + """Evaluate a benchmark session. + + Args: + session_id: Session identifier + + Returns: + True if session was successful, False otherwise + + Raises: + RuntimeError: If adapter not initialized or evaluation fails + """ + if not self._initialized: + raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") + + logger.info(f"Evaluating session: {session_id}") + + try: + result = self.mcp_client.evaluate_session(session_id) + success = result.get("success", False) + logger.info(f"Session {session_id} evaluation: {'success' if success else 'failed'}") + return success + + except Exception as e: + logger.error(f"Failed to evaluate session {session_id}: {e}") + raise + + def close_session(self, session_id: str) -> None: + """Close a benchmark session. + + Args: + session_id: Session identifier + + Raises: + RuntimeError: If adapter not initialized or close fails + """ + if not self._initialized: + raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") + + logger.info(f"Closing session: {session_id}") + + try: + self.mcp_client.close_session(session_id) + logger.info(f"Session {session_id} closed successfully") + + except Exception as e: + logger.error(f"Failed to close session {session_id}: {e}") + raise + + def iterate_sessions(self) -> Iterator[SessionData]: + """Iterate over benchmark sessions. + + Creates sessions one at a time up to max_tasks limit. + + Yields: + SessionData for each session + """ + if not self._initialized: + raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") + + session_num = 0 + max_tasks = self.config.max_tasks + + while True: + # Check if we've reached the limit + if max_tasks is not None and session_num >= max_tasks: + logger.info(f"Reached max_tasks limit: {max_tasks}") + break + + try: + session_data = self.create_session() + session_num += 1 + yield session_data + + except Exception as e: + logger.error(f"Failed to create session {session_num + 1}: {e}") + raise + + +# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py new file mode 100644 index 0000000..00489c5 --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py @@ -0,0 +1,247 @@ +"""MCP client for communicating with Exgentic MCP server. + +Uses streamable HTTP transport to interact with the Exgentic benchmark server. +""" + +import asyncio +import logging +from typing import Any, Dict, Optional, Tuple + +from mcp import ClientSession +from mcp.client.streamable_http import streamable_http_client + +from .config import ExgenticConfig + +logger = logging.getLogger(__name__) + + +class MCPClient: + """Client for MCP protocol communication with Exgentic server via streamable HTTP.""" + + def __init__(self, config: ExgenticConfig): + """Initialize MCP client. + + Args: + config: Exgentic configuration + """ + self.config = config + self.mcp_url = config.mcp_server_url + self.session: Optional[ClientSession] = None + self._initialized = False + + logger.info(f"Initialized MCP client for {self.mcp_url}") + + def initialize(self) -> None: + """Initialize MCP client connection. + + Raises: + RuntimeError: If connection fails + """ + logger.info(f"Initializing MCP client for {self.mcp_url}") + + try: + # Run async initialization + asyncio.run(self._async_initialize()) + self._initialized = True + logger.info("MCP client initialized successfully") + + except Exception as e: + logger.error(f"Failed to initialize MCP client: {e}") + raise RuntimeError(f"MCP client initialization failed: {e}") + + async def _async_initialize(self) -> None: + """Async initialization of MCP client.""" + # Create streamable HTTP client + async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): + async with ClientSession(read, write) as session: + self.session = session + + # Initialize the session + await session.initialize() + + # List available tools to verify connection + tools_result = await session.list_tools() + logger.info(f"Connected to MCP server with {len(tools_result.tools)} tools available") + + def shutdown(self) -> None: + """Shutdown MCP client connection.""" + if self._initialized: + try: + # Session cleanup is handled by context managers + logger.info("MCP client shutdown complete") + except Exception as e: + logger.warning(f"Error during MCP client shutdown: {e}") + self._initialized = False + + def create_session(self, task_id: Optional[str] = None) -> Tuple[str, str]: + """Create a new benchmark session. + + Args: + task_id: Optional task ID. If not provided, will use the first available task. + + Returns: + Tuple of (session_id, task_description) + + Raises: + RuntimeError: If session creation fails + """ + if not self._initialized: + raise RuntimeError("MCP client not initialized") + + logger.info("Creating new benchmark session") + + try: + # If no task_id provided, list tasks and use the first one + if task_id is None: + tasks = asyncio.run(self._async_list_tasks()) + if not tasks: + raise RuntimeError("No tasks available") + # tasks is a list of task ID strings, not dicts + task_id = tasks[0] + logger.info(f"Using first available task: {task_id}") + + # At this point task_id is guaranteed to be a string + assert task_id is not None, "task_id should not be None" + result = asyncio.run(self._async_create_session(task_id)) + session_id = result["session_id"] + task = result.get("task", result.get("task_description", "")) + + logger.info(f"Created session {session_id} for task {task_id}") + return session_id, task + + except Exception as e: + logger.error(f"Failed to create session: {e}") + raise RuntimeError(f"Session creation failed: {e}") + + async def _async_list_tasks(self) -> list: + """Async task listing.""" + async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): + async with ClientSession(read, write) as session: + await session.initialize() + + # Call list_tasks tool + result = await session.call_tool("list_tasks", arguments={}) + + if not result.content: + raise RuntimeError("Empty response from list_tasks") + + # Extract result from content + content = result.content[0] + if hasattr(content, 'text'): + import json + data = json.loads(content.text) + # tasks is a list of task ID strings + return data.get("tasks", []) + else: + raise RuntimeError(f"Unexpected content type: {type(content)}") + + async def _async_create_session(self, task_id: str) -> Dict[str, Any]: + """Async session creation.""" + async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): + async with ClientSession(read, write) as session: + await session.initialize() + + # Call create_session tool with task_id + result = await session.call_tool("create_session", arguments={"task_id": task_id}) + + if not result.content: + raise RuntimeError("Empty response from create_session") + + # Check if it's an error response + if result.isError: + content = result.content[0] + error_msg = content.text if hasattr(content, 'text') else str(content) + raise RuntimeError(f"MCP tool error: {error_msg}") + + # Extract result from content + content = result.content[0] + if hasattr(content, 'text'): + import json + return json.loads(content.text) + else: + raise RuntimeError(f"Unexpected content type: {type(content)}") + + def evaluate_session(self, session_id: str) -> Dict[str, Any]: + """Evaluate a benchmark session. + + Args: + session_id: Session ID to evaluate + + Returns: + Evaluation results + + Raises: + RuntimeError: If evaluation fails + """ + if not self._initialized: + raise RuntimeError("MCP client not initialized") + + logger.info(f"Evaluating session {session_id}") + + try: + result = asyncio.run(self._async_evaluate_session(session_id)) + logger.info(f"Session {session_id} evaluation complete") + return result + + except Exception as e: + logger.error(f"Failed to evaluate session {session_id}: {e}") + raise RuntimeError(f"Session evaluation failed: {e}") + + async def _async_evaluate_session(self, session_id: str) -> Dict[str, Any]: + """Async session evaluation.""" + async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): + async with ClientSession(read, write) as session: + await session.initialize() + + # Call evaluate_session tool + result = await session.call_tool( + "evaluate_session", + arguments={"session_id": session_id} + ) + + if not result.content: + raise RuntimeError("Empty response from evaluate_session") + + # Extract result from content + content = result.content[0] + if hasattr(content, 'text'): + import json + return json.loads(content.text) + else: + raise RuntimeError(f"Unexpected content type: {type(content)}") + + def close_session(self, session_id: str) -> None: + """Close a benchmark session. + + Args: + session_id: Session ID to close + + Raises: + RuntimeError: If session closure fails + """ + if not self._initialized: + raise RuntimeError("MCP client not initialized") + + logger.info(f"Closing session {session_id}") + + try: + asyncio.run(self._async_close_session(session_id)) + logger.info(f"Session {session_id} closed") + + except Exception as e: + logger.error(f"Failed to close session {session_id}: {e}") + raise RuntimeError(f"Session closure failed: {e}") + + async def _async_close_session(self, session_id: str) -> None: + """Async session closure.""" + async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): + async with ClientSession(read, write) as session: + await session.initialize() + + # Call close_session tool + await session.call_tool( + "close_session", + arguments={"session_id": session_id} + ) + +# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/otel.py b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py new file mode 100644 index 0000000..395f1df --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py @@ -0,0 +1,372 @@ +"""OpenTelemetry instrumentation for Exgentic A2A Runner. + +Provides traces, metrics, and logs for monitoring session execution. +""" + +import logging +import time +from contextlib import contextmanager +from typing import Iterator, Optional + +from opentelemetry import metrics, trace +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.trace import Status, StatusCode + +from .config import OTELConfig + +logger = logging.getLogger(__name__) + + +class OTELInstrumentation: + """OpenTelemetry instrumentation manager.""" + + def __init__(self, config: OTELConfig): + """Initialize OTEL instrumentation. + + Args: + config: OTEL configuration + """ + self.config = config + self.tracer: Optional[trace.Tracer] = None + self.meter: Optional[metrics.Meter] = None + self._trace_provider: Optional[TracerProvider] = None + self._meter_provider: Optional[MeterProvider] = None + + # Metrics + self.sessions_counter: Optional[metrics.Counter] = None + self.errors_counter: Optional[metrics.Counter] = None + self.session_latency_histogram: Optional[metrics.Histogram] = None + self.evaluation_latency_histogram: Optional[metrics.Histogram] = None + self.session_creation_latency_histogram: Optional[metrics.Histogram] = None + self.a2a_latency_histogram: Optional[metrics.Histogram] = None + self.prompt_size_histogram: Optional[metrics.Histogram] = None + self.response_size_histogram: Optional[metrics.Histogram] = None + self.inflight_sessions_gauge: Optional[metrics.UpDownCounter] = None + self._requests_instrumented = False + + def initialize(self) -> None: + """Initialize OTEL providers and instruments.""" + logger.info("Initializing OpenTelemetry instrumentation") + + # Create resource with service name and attributes + resource_attrs = {"service.name": self.config.service_name} + if self.config.resource_attributes: + # Parse resource attributes (format: key1=val1,key2=val2) + for attr in self.config.resource_attributes.split(","): + if "=" in attr: + key, value = attr.split("=", 1) + resource_attrs[key.strip()] = value.strip() + + resource = Resource.create(resource_attrs) + + # Initialize tracing + self._initialize_tracing(resource) + + # Initialize auto-instrumentation + self._initialize_auto_instrumentation() + + # Initialize metrics + self._initialize_metrics(resource) + + logger.info("OpenTelemetry instrumentation initialized") + + def _initialize_tracing(self, resource: Resource) -> None: + """Initialize tracing provider and exporter.""" + trace_provider = TracerProvider(resource=resource) + + if self.config.exporter_endpoint: + # Use OTLP exporter + logger.info(f"Configuring OTLP trace exporter: {self.config.exporter_endpoint}") + span_exporter = OTLPSpanExporter( + endpoint=self.config.exporter_endpoint, + insecure=self.config.exporter_insecure, + ) + else: + # Use console exporter for development + logger.info("Using console trace exporter (no OTLP endpoint configured)") + span_exporter = ConsoleSpanExporter() + + trace_provider.add_span_processor(BatchSpanProcessor(span_exporter)) + trace.set_tracer_provider(trace_provider) + self._trace_provider = trace_provider + + self.tracer = trace.get_tracer(__name__) + + def _initialize_auto_instrumentation(self) -> None: + """Initialize opt-in auto instrumentation.""" + if not self.config.instrument_requests: + logger.info("Requests auto-instrumentation disabled") + return + + if self._requests_instrumented: + return + + RequestsInstrumentor().instrument() + self._requests_instrumented = True + logger.info("Enabled OpenTelemetry requests auto-instrumentation") + + def _initialize_metrics(self, resource: Resource) -> None: + """Initialize metrics provider and instruments.""" + metric_readers = [] + + if self.config.exporter_endpoint: + # Use OTLP exporter + logger.info(f"Configuring OTLP metric exporter: {self.config.exporter_endpoint}") + metric_exporter = OTLPMetricExporter( + endpoint=self.config.exporter_endpoint, + insecure=self.config.exporter_insecure, + ) + metric_reader = PeriodicExportingMetricReader(metric_exporter) + metric_readers.append(metric_reader) + else: + # No exporter configured - metrics will be collected but not exported + logger.info("No OTLP endpoint configured, metrics will not be exported") + + meter_provider = MeterProvider( + resource=resource, + metric_readers=metric_readers, + ) + metrics.set_meter_provider(meter_provider) + self._meter_provider = meter_provider + + self.meter = metrics.get_meter(__name__) + + # Create metric instruments + self.sessions_counter = self.meter.create_counter( + name="exgentic_a2a_sessions_total", + description="Total number of sessions processed", + unit="1", + ) + + self.errors_counter = self.meter.create_counter( + name="exgentic_a2a_errors_total", + description="Total number of errors", + unit="1", + ) + + self.session_latency_histogram = self.meter.create_histogram( + name="exgentic_a2a_session_latency_ms", + description="Session processing latency in milliseconds", + unit="ms", + ) + + self.evaluation_latency_histogram = self.meter.create_histogram( + name="exgentic_a2a_evaluation_latency_ms", + description="Session evaluation latency in milliseconds", + unit="ms", + ) + + self.session_creation_latency_histogram = self.meter.create_histogram( + name="exgentic_a2a_session_creation_latency_ms", + description="Session creation latency in milliseconds", + unit="ms", + ) + + self.a2a_latency_histogram = self.meter.create_histogram( + name="exgentic_a2a_a2a_latency_ms", + description="A2A request latency in milliseconds", + unit="ms", + ) + + self.prompt_size_histogram = self.meter.create_histogram( + name="exgentic_a2a_prompt_size_chars", + description="Prompt size in characters", + unit="chars", + ) + + self.response_size_histogram = self.meter.create_histogram( + name="exgentic_a2a_response_size_chars", + description="Response size in characters", + unit="chars", + ) + + self.inflight_sessions_gauge = self.meter.create_up_down_counter( + name="exgentic_a2a_inflight_sessions", + description="Number of sessions currently in flight", + unit="1", + ) + + def shutdown(self) -> None: + """Shut down OTEL providers to flush pending spans and metrics.""" + logger.info("Shutting down OpenTelemetry providers") + if self._trace_provider: + self._trace_provider.shutdown() + if self._meter_provider: + self._meter_provider.shutdown() + + @contextmanager + def session_span( + self, + session_id: str, + mcp_server_url: str, + a2a_base_url: str, + a2a_timeout: int, + ) -> Iterator[trace.Span]: + """Create a span for session processing. + + Args: + session_id: Session identifier + mcp_server_url: MCP server URL + a2a_base_url: A2A endpoint URL (sanitized) + a2a_timeout: A2A timeout in seconds + + Yields: + Span object for adding events and attributes + """ + if not self.tracer: + raise RuntimeError("OTEL not initialized") + + # Increment inflight gauge + if self.inflight_sessions_gauge: + self.inflight_sessions_gauge.add(1) + + start_time = time.time() + + with self.tracer.start_as_current_span("exgentic_a2a.session") as span: + # Set span attributes + span.set_attribute("exgentic.session_id", session_id) + span.set_attribute("exgentic.mcp_server_url", mcp_server_url) + span.set_attribute("a2a.base_url", a2a_base_url) + span.set_attribute("a2a.timeout_seconds", a2a_timeout) + + try: + yield span + finally: + # Decrement inflight gauge + if self.inflight_sessions_gauge: + self.inflight_sessions_gauge.add(-1) + + # Record session latency + latency_ms = (time.time() - start_time) * 1000 + if self.session_latency_histogram: + self.session_latency_histogram.record(latency_ms) + + @contextmanager + def child_span(self, name: str) -> Iterator[trace.Span]: + """Create a child span under the current context.""" + if not self.tracer: + raise RuntimeError("OTEL not initialized") + + with self.tracer.start_as_current_span(name) as span: + yield span + + def record_prompt(self, span: trace.Span, prompt: str) -> None: + """Record prompt information. + + Args: + span: Current span + prompt: Prompt text + """ + prompt_chars = len(prompt) + span.set_attribute("prompt.chars", prompt_chars) + span.add_event("prompt_built") + + if self.prompt_size_histogram: + self.prompt_size_histogram.record(prompt_chars) + + def record_a2a_request( + self, + span: trace.Span, + duration_ms: float, + ) -> None: + """Record A2A request metrics. + + Args: + span: Current span + duration_ms: Request duration in milliseconds + """ + span.set_attribute("a2a.duration_ms", duration_ms) + + if self.a2a_latency_histogram: + self.a2a_latency_histogram.record(duration_ms) + + def record_response(self, span: trace.Span, response: str) -> None: + """Record response information. + + Args: + span: Current span + response: Response text + """ + response_chars = len(response) + span.set_attribute("response.chars", response_chars) + + if self.response_size_histogram: + self.response_size_histogram.record(response_chars) + + def record_success(self, span: trace.Span, evaluation_result: bool) -> None: + """Record successful session completion. + + Args: + span: Current span + evaluation_result: Whether the session evaluation was successful + """ + span.set_attribute("session.status", "success") + span.set_attribute("exgentic.evaluation_result", evaluation_result) + span.set_status(Status(StatusCode.OK)) + + if self.sessions_counter: + self.sessions_counter.add(1, {"status": "success"}) + + def record_failure( + self, + span: trace.Span, + error: Exception, + error_type: str, + ) -> None: + """Record session failure. + + Args: + span: Current span + error: Exception that caused failure + error_type: Error type classification + """ + span.set_attribute("session.status", "failed") + span.add_event( + "session_failed", + attributes={ + "error.type": error_type, + "error.message": str(error), + }, + ) + span.set_status(Status(StatusCode.ERROR, str(error))) + span.record_exception(error) + + if self.sessions_counter: + self.sessions_counter.add(1, {"status": "failed"}) + + if self.errors_counter: + self.errors_counter.add(1, {"error_type": error_type}) + + def record_evaluation(self, span: trace.Span, duration_ms: float) -> None: + """Record session evaluation metrics. + + Args: + span: Current span + duration_ms: Evaluation duration in milliseconds + """ + span.set_attribute("exgentic.evaluation_duration_ms", duration_ms) + + if self.evaluation_latency_histogram: + self.evaluation_latency_histogram.record(duration_ms) + + def record_session_creation(self, span: trace.Span, duration_ms: float) -> None: + """Record session creation metrics. + + Args: + span: Current span + duration_ms: Creation duration in milliseconds + """ + span.set_attribute("exgentic.session_creation_duration_ms", duration_ms) + + if self.session_creation_latency_histogram: + self.session_creation_latency_histogram.record(duration_ms) + + +# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py new file mode 100644 index 0000000..6503540 --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py @@ -0,0 +1,31 @@ +"""Prompt construction for Exgentic A2A Runner. + +Builds prompts with session_id for agent to use with benchmark tools. +""" + + +def build_prompt(task: str, session_id: str) -> str: + """Build prompt with task and session_id. + + The prompt format includes the task description and explicitly instructs + the agent to use the provided session_id in all interactions with the + benchmark tools. + + Args: + task: Task description from Exgentic MCP server + session_id: Session identifier to use for tool calls + + Returns: + Formatted prompt string + """ + prompt = f"""The task you are to complete is: +{task} + +IMPORTANT: Use session id "{session_id}" in all your interactions with the benchmark tools. + +When calling any benchmark-related tools or APIs, you MUST include the session_id parameter with the value "{session_id}". This ensures your actions are properly tracked and evaluated within the correct benchmark session.""" + + return prompt + + +# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py new file mode 100644 index 0000000..694b819 --- /dev/null +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -0,0 +1,365 @@ +"""Main runner for Exgentic A2A Runner. + +Orchestrates session creation, A2A calls, evaluation, and telemetry collection. +""" + +import argparse +import logging +import sys +import time +from typing import List, Optional + +from .a2a_client import A2AProxyClient +from .config import Config +from .exgentic_adapter import ExgenticAdapter, SessionData +from .otel import OTELInstrumentation +from .prompt import build_prompt + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Suppress verbose logs from third-party libraries +logging.getLogger("httpcore").setLevel(logging.WARNING) +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("urllib3").setLevel(logging.WARNING) +logging.getLogger("mcp.client.streamable_http").setLevel(logging.INFO) + + +class SessionResult: + """Result of a single session execution.""" + + def __init__( + self, + session_id: str, + success: bool, + latency_ms: float, + evaluation_result: bool, + error: Optional[str] = None, + response_chars: Optional[int] = None, + ): + self.session_id = session_id + self.success = success + self.latency_ms = latency_ms + self.evaluation_result = evaluation_result + self.error = error + self.response_chars = response_chars + + +class RunSummary: + """Summary of the entire run.""" + + def __init__(self): + self.start_time = time.time() + self.results: List[SessionResult] = [] + + def add_result(self, result: SessionResult) -> None: + """Add a session result.""" + self.results.append(result) + + def get_summary(self) -> dict: + """Get summary statistics.""" + total_time = time.time() - self.start_time + attempted = len(self.results) + succeeded = sum(1 for r in self.results if r.success) + failed = attempted - succeeded + + # Calculate evaluation success rate + evaluated = sum(1 for r in self.results if r.success) + eval_succeeded = sum(1 for r in self.results if r.success and r.evaluation_result) + eval_success_rate = (eval_succeeded / evaluated * 100) if evaluated > 0 else 0 + + latencies = [r.latency_ms for r in self.results] + avg_latency = sum(latencies) / len(latencies) if latencies else 0 + + # Calculate percentiles + sorted_latencies = sorted(latencies) + p50 = sorted_latencies[len(sorted_latencies) // 2] if sorted_latencies else 0 + p95_idx = min(int(len(sorted_latencies) * 0.95), len(sorted_latencies) - 1) + p95 = sorted_latencies[p95_idx] if sorted_latencies else 0 + + return { + "sessions_attempted": attempted, + "sessions_succeeded": succeeded, + "sessions_failed": failed, + "evaluation_success_rate": eval_success_rate, + "total_wall_time_seconds": total_time, + "average_latency_ms": avg_latency, + "p50_latency_ms": p50, + "p95_latency_ms": p95, + } + + def print_summary(self) -> None: + """Print summary to console.""" + summary = self.get_summary() + + print("\n" + "=" * 60) + print("RUN SUMMARY") + print("=" * 60) + print(f"Sessions Attempted: {summary['sessions_attempted']}") + print(f"Sessions Succeeded: {summary['sessions_succeeded']}") + print(f"Sessions Failed: {summary['sessions_failed']}") + print(f"Evaluation Success: {summary['evaluation_success_rate']:.1f}%") + print(f"Total Wall Time: {summary['total_wall_time_seconds']:.2f}s") + print(f"Average Latency: {summary['average_latency_ms']:.2f}ms") + print(f"P50 Latency: {summary['p50_latency_ms']:.2f}ms") + print(f"P95 Latency: {summary['p95_latency_ms']:.2f}ms") + print("=" * 60 + "\n") + + +class Runner: + """Main runner orchestrating session execution.""" + + def __init__(self, config: Config): + """Initialize runner. + + Args: + config: Complete configuration + """ + self.config = config + self.exgentic = ExgenticAdapter(config.exgentic) + self.a2a_client = A2AProxyClient(config.a2a) + self.otel = OTELInstrumentation(config.otel) + self.summary = RunSummary() + + def initialize(self) -> None: + """Initialize all components.""" + logger.info("Initializing runner components") + self.otel.initialize() + self.exgentic.initialize() + logger.info("Runner initialization complete") + + def process_session(self, session_data: SessionData) -> SessionResult: + """Process a single session. + + Follows the execution model from GitHub issue #963: + 1. Create session (already done) + 2. Build prompt with session_id + 3. Send to agent via A2A + 4. Evaluate session + 5. Close session + 6. Record statistics + + Args: + session_data: Session data from Exgentic + + Returns: + SessionResult with execution details + """ + session_id = session_data.session_id + start_time = time.time() + + logger.info(f"Processing session: {session_id}") + + # Start OTEL span + with self.otel.session_span( + session_id=session_id, + mcp_server_url=self.config.exgentic.mcp_server_url, + a2a_base_url=self.config.a2a.base_url, + a2a_timeout=self.config.a2a.timeout_seconds, + ) as span: + try: + # Build prompt with session_id + with self.otel.child_span("exgentic_a2a.prompt.build"): + prompt = build_prompt(session_data.task, session_data.session_id) + self.otel.record_prompt(span, prompt) + + if self.config.debug.log_prompt: + logger.debug(f"Prompt length: {len(prompt)} chars") + + # Send A2A request + a2a_start = time.time() + with self.otel.child_span("exgentic_a2a.a2a.send_prompt"): + response = self.a2a_client.send_prompt(prompt) + a2a_duration_ms = (time.time() - a2a_start) * 1000 + + self.otel.record_a2a_request(span, a2a_duration_ms) + self.otel.record_response(span, response) + + if self.config.debug.log_response: + logger.debug(f"Response length: {len(response)} chars") + + # Evaluate session + eval_start = time.time() + with self.otel.child_span("exgentic_a2a.mcp.evaluate_session"): + evaluation_result = self.exgentic.evaluate_session(session_id) + eval_duration_ms = (time.time() - eval_start) * 1000 + self.otel.record_evaluation(span, eval_duration_ms) + + # Close session + with self.otel.child_span("exgentic_a2a.mcp.close_session"): + self.exgentic.close_session(session_id) + + # Record success + self.otel.record_success(span, evaluation_result) + + latency_ms = (time.time() - start_time) * 1000 + logger.info( + f"Session {session_id} completed in {latency_ms:.2f}ms " + f"(evaluation: {'success' if evaluation_result else 'failed'})" + ) + + return SessionResult( + session_id=session_id, + success=True, + latency_ms=latency_ms, + evaluation_result=evaluation_result, + response_chars=len(response), + ) + + except Exception as e: + # Classify error type + error_type = type(e).__name__ + error_msg = str(e) + + logger.error(f"Session {session_id} failed: {error_type}: {error_msg}") + + # Try to close session even on failure + try: + with self.otel.child_span("exgentic_a2a.mcp.close_session"): + self.exgentic.close_session(session_id) + except Exception as close_error: + logger.warning(f"Failed to close session {session_id}: {close_error}") + + # Record failure + self.otel.record_failure(span, e, error_type) + + latency_ms = (time.time() - start_time) * 1000 + + return SessionResult( + session_id=session_id, + success=False, + latency_ms=latency_ms, + evaluation_result=False, + error=f"{error_type}: {error_msg}", + ) + + def run(self) -> int: + """Run the session processing loop. + + Returns: + Exit code (0 for success, 1 for failure) + """ + try: + self.initialize() + + # Process sessions sequentially + for session_data in self.exgentic.iterate_sessions(): + # Record session creation time + creation_time_ms = (time.time() - session_data.created_at) * 1000 + # Note: We can't easily add this to a span since it's created before the span + # but we can log it + logger.debug(f"Session creation took {creation_time_ms:.2f}ms") + + result = self.process_session(session_data) + self.summary.add_result(result) + + # Check abort on failure + if not result.success and self.config.exgentic.abort_on_failure: + logger.error("Aborting due to session failure (ABORT_ON_FAILURE=true)") + break + + # Print summary + self.summary.print_summary() + + # Return success if at least one session succeeded + if any(r.success for r in self.summary.results): + return 0 + else: + return 1 + + except Exception as e: + logger.exception(f"Fatal error in runner: {e}") + return 1 + finally: + # Shutdown components + try: + self.exgentic.shutdown() + except Exception as e: + logger.warning(f"Error shutting down Exgentic adapter: {e}") + + self.otel.shutdown() + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments. + + Returns: + Parsed arguments + """ + parser = argparse.ArgumentParser( + description="Exgentic A2A Runner", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Environment Variables: + EXGENTIC_MCP_SERVER_URL MCP server endpoint (required) + EXGENTIC_MCP_TIMEOUT_SECONDS MCP timeout in seconds (default: 60) + MAX_TASKS Maximum number of sessions to process + ABORT_ON_FAILURE Stop on first failure (default: false) + + A2A_BASE_URL A2A endpoint base URL (required) + A2A_TIMEOUT_SECONDS Request timeout in seconds (default: 300) + A2A_AUTH_TOKEN Bearer token for authentication + A2A_VERIFY_TLS Verify TLS certificates (default: true) + A2A_ENDPOINT_PATH Endpoint path (default: /v1/chat) + + OTEL_SERVICE_NAME Service name for telemetry (default: exgentic-a2a-runner) + OTEL_EXPORTER_OTLP_ENDPOINT OTLP exporter endpoint + OTEL_EXPORTER_OTLP_PROTOCOL OTLP protocol (default: grpc) + OTEL_RESOURCE_ATTRIBUTES Additional resource attributes + OTEL_EXPORTER_OTLP_INSECURE Use insecure connection for OTLP (default: true) + + LOG_PROMPT Log prompt details (default: 0) + LOG_RESPONSE Log response details (default: 0) + """, + ) + + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose logging", + ) + + return parser.parse_args() + + +def main() -> int: + """Main entry point. + + Returns: + Exit code + """ + args = parse_args() + + # Set log level + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + try: + # Load configuration from environment + config = Config.from_env() + + # Create and run + runner = Runner(config) + return runner.run() + + except ValueError as e: + logger.error(f"Configuration error: {e}") + return 1 + except KeyboardInterrupt: + logger.info("Interrupted by user") + return 130 + except Exception as e: + logger.exception(f"Unexpected error: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) + + +# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/pyproject.toml b/exgentic_a2a_runner/pyproject.toml new file mode 100644 index 0000000..4523772 --- /dev/null +++ b/exgentic_a2a_runner/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "exgentic-a2a-runner" +version = "0.1.0" +description = "Exgentic Benchmark A2A Runner for Kagenti" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "mcp>=0.9.0", + "requests>=2.28.0", + "opentelemetry-api>=1.20.0", + "opentelemetry-sdk>=1.20.0", + "opentelemetry-exporter-otlp>=1.20.0", + "opentelemetry-instrumentation-requests>=0.41b0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-mock>=3.10.0", + "black>=23.0.0", + "mypy>=1.0.0", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["exgentic_a2a_runner*"] + +[project.scripts] +exgentic-a2a-runner = "exgentic_a2a_runner.runner:main" \ No newline at end of file diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh new file mode 100755 index 0000000..8fd4a13 --- /dev/null +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Helper script to set up port forwarding and run the Exgentic A2A Runner + +set -e + +echo "==========================================" +echo "Exgentic A2A Runner - Port Forward Setup" +echo "==========================================" +echo "" + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "Error: kubectl is not installed or not in PATH" + exit 1 +fi + +# Check if we're connected to the right cluster +CURRENT_CONTEXT=$(kubectl config current-context) +echo "Current kubectl context: $CURRENT_CONTEXT" + +if [ "$CURRENT_CONTEXT" != "kind-kagenti" ]; then + echo "Warning: Not connected to kind-kagenti cluster" + read -p "Continue anyway? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +echo "" +echo "Setting up port forwarding..." +echo " - MCP Server: localhost:8000 -> exgentic-mcp-tau2-mcp.team1:8000" +echo " - A2A Agent: localhost:8080 -> generic-agent2.team1:8080" +echo "" + +# Kill any existing port-forwards on these ports +echo "Cleaning up existing port-forwards..." +pkill -f "port-forward.*exgentic-mcp-tau2-mcp" 2>/dev/null || true +pkill -f "port-forward.*generic-agent2" 2>/dev/null || true +sleep 2 + +# Start port forwarding in background +echo "Starting port-forward for MCP server..." +kubectl port-forward -n team1 svc/exgentic-mcp-tau2-mcp 8000:8000 & +PF_MCP_PID=$! + +echo "Starting port-forward for A2A agent..." +kubectl port-forward -n team1 svc/generic-agent2 8080:8080 & +PF_AGENT_PID=$! + +# Wait for port forwards to be ready +echo "Waiting for port forwards to be ready..." +sleep 3 + +# Check if port forwards are working +if ! ps -p $PF_MCP_PID > /dev/null; then + echo "Error: MCP port-forward failed to start" + exit 1 +fi + +if ! ps -p $PF_AGENT_PID > /dev/null; then + echo "Error: Agent port-forward failed to start" + kill $PF_MCP_PID 2>/dev/null || true + exit 1 +fi + +echo "" +echo "✓ Port forwarding established" +echo " MCP Server PID: $PF_MCP_PID" +echo " A2A Agent PID: $PF_AGENT_PID" +echo "" + +# Function to cleanup on exit +cleanup() { + echo "" + echo "Cleaning up port forwards..." + kill $PF_MCP_PID 2>/dev/null || true + kill $PF_AGENT_PID 2>/dev/null || true + echo "Done." +} + +trap cleanup EXIT INT TERM + +# Test connectivity +echo "Testing connectivity..." +echo -n " MCP Server: " +if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null | grep -q "200\|404"; then + echo "✓ Reachable" +else + echo "⚠ May not be reachable (this might be OK if no /health endpoint)" +fi + +echo -n " A2A Agent: " +if curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/.well-known/agent-card.json 2>/dev/null | grep -q "200\|404"; then + echo "✓ Reachable" +else + echo "⚠ May not be reachable (this might be OK)" +fi + +echo "" +echo "==========================================" +echo "Starting Exgentic A2A Runner" +echo "==========================================" +echo "" + +# Change to the script directory +cd "$(dirname "$0")" + +# Check if virtual environment exists +if [ ! -d ".venv" ]; then + echo "Virtual environment not found. Installing dependencies..." + uv sync --python 3.12 +fi + +# Activate virtual environment and run +source .venv/bin/activate + +# Load environment variables +if [ -f ".env" ]; then + echo "Loading environment variables from .env" + export $(cat .env | grep -v '^#' | xargs) + echo "" +fi + +# Run the harness +echo "Running: uv run exgentic-a2a-runner --verbose" +echo "" +uv run exgentic-a2a-runner --verbose + +# Cleanup will happen automatically via trap + +# Made with Bob diff --git a/exgentic_a2a_runner/uv.lock b/exgentic_a2a_runner/uv.lock new file mode 100644 index 0000000..9387fae --- /dev/null +++ b/exgentic_a2a_runner/uv.lock @@ -0,0 +1,1406 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[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 = "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 = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[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/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 = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[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 = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "exgentic-a2a-runner" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "mcp", specifier = ">=0.9.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "opentelemetry-api", specifier = ">=1.20.0" }, + { name = "opentelemetry-exporter-otlp", specifier = ">=1.20.0" }, + { name = "opentelemetry-instrumentation-requests", specifier = ">=0.41b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.20.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" }, + { name = "requests", specifier = ">=2.28.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "googleapis-common-protos" +version = "1.73.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "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 = "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 = "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.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.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/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[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 = "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/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 = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/c7/7a47cb85c7aa93a9c820552e414889185bcf91245271d12e5d443e5f834d/opentelemetry_instrumentation_requests-0.61b0.tar.gz", hash = "sha256:15f879ce8fb206bd7e6fdc61663ea63481040a845218c0cf42902ce70bd7e9d9", size = 18379, upload-time = "2026-03-04T14:20:46.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/a1/a7a133b273d1f53950f16a370fc94367eff472c9c2576e8e9e28c62dcc9f/opentelemetry_instrumentation_requests-0.61b0-py3-none-any.whl", hash = "sha256:cce19b379949fe637eb73ba39b02c57d2d0805447ca6d86534aa33fcb141f683", size = 14207, upload-time = "2026-03-04T14:19:51.765Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[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 = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[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/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/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.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[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.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +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-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { 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 = "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 = "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/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 = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[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 = "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.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[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" }, +] From a0ab3685b48e27100d1125cc8929dac36a355405 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Wed, 18 Mar 2026 10:10:16 +0200 Subject: [PATCH 02/17] refactor: Fetch task list upfront and iterate over it Changes: - Add list_tasks() method to MCPClient to fetch all available task IDs - Add get_task_ids() method to ExgenticAdapter - Update iterate_sessions() to accept task_ids list and respect max_tasks - Update create_session() to accept optional task_id parameter - Update runner to fetch task IDs first, then iterate over them - Remove debug exit(99) statement - Improve logging to show progress (task X/Y) This ensures we know the total number of tasks upfront and can properly limit processing with max_tasks configuration. Signed-off-by: Yoav Katz --- .../exgentic_a2a_runner/exgentic_adapter.py | 56 +++++++++++++------ .../exgentic_a2a_runner/mcp_client.py | 23 ++++++++ .../exgentic_a2a_runner/runner.py | 12 +++- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py index 1b9e2ce..e888957 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py @@ -51,9 +51,12 @@ def shutdown(self) -> None: self.mcp_client.shutdown() self._initialized = False - def create_session(self) -> SessionData: + def create_session(self, task_id: Optional[str] = None) -> SessionData: """Create a new benchmark session. + Args: + task_id: Optional task ID. If not provided, will use the first available task. + Returns: SessionData containing session_id and task @@ -63,11 +66,11 @@ def create_session(self) -> SessionData: if not self._initialized: raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") - logger.info("Creating new session") + logger.info(f"Creating new session{f' for task {task_id}' if task_id else ''}") created_at = time.time() try: - session_id, task = self.mcp_client.create_session() + session_id, task = self.mcp_client.create_session(task_id=task_id) self._session_count += 1 logger.info(f"Created session {self._session_count}: {session_id}") @@ -131,10 +134,30 @@ def close_session(self, session_id: str) -> None: logger.error(f"Failed to close session {session_id}: {e}") raise - def iterate_sessions(self) -> Iterator[SessionData]: - """Iterate over benchmark sessions. + def get_task_ids(self) -> list[str]: + """Get list of all available task IDs from the MCP server. + + Returns: + List of task ID strings - Creates sessions one at a time up to max_tasks limit. + Raises: + RuntimeError: If adapter not initialized or task listing fails + """ + if not self._initialized: + raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") + + logger.info("Fetching list of available tasks") + task_ids = self.mcp_client.list_tasks() + logger.info(f"Found {len(task_ids)} available tasks") + return task_ids + + def iterate_sessions(self, task_ids: list[str]) -> Iterator[SessionData]: + """Iterate over benchmark sessions for given task IDs. + + Creates sessions sequentially for each task ID, respecting max_tasks configuration. + + Args: + task_ids: List of task IDs to process Yields: SessionData for each session @@ -142,22 +165,23 @@ def iterate_sessions(self) -> Iterator[SessionData]: if not self._initialized: raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") - session_num = 0 max_tasks = self.config.max_tasks - while True: - # Check if we've reached the limit - if max_tasks is not None and session_num >= max_tasks: - logger.info(f"Reached max_tasks limit: {max_tasks}") - break - + # Limit task_ids if max_tasks is set + if max_tasks is not None: + task_ids = task_ids[:max_tasks] + logger.info(f"Processing {len(task_ids)} tasks (limited by max_tasks={max_tasks})") + else: + logger.info(f"Processing all {len(task_ids)} tasks") + + for idx, task_id in enumerate(task_ids, 1): try: - session_data = self.create_session() - session_num += 1 + logger.info(f"Creating session {idx}/{len(task_ids)} for task {task_id}") + session_data = self.create_session(task_id=task_id) yield session_data except Exception as e: - logger.error(f"Failed to create session {session_num + 1}: {e}") + logger.error(f"Failed to create session {idx} for task {task_id}: {e}") raise diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py index 00489c5..2fc7d9d 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py @@ -73,6 +73,29 @@ def shutdown(self) -> None: logger.warning(f"Error during MCP client shutdown: {e}") self._initialized = False + def list_tasks(self) -> list[str]: + """List all available task IDs. + + Returns: + List of task ID strings + + Raises: + RuntimeError: If task listing fails + """ + if not self._initialized: + raise RuntimeError("MCP client not initialized") + + logger.info("Listing available tasks") + + try: + tasks = asyncio.run(self._async_list_tasks()) + logger.info(f"Found {len(tasks)} tasks") + return tasks + + except Exception as e: + logger.error(f"Failed to list tasks: {e}") + raise RuntimeError(f"Task listing failed: {e}") + def create_session(self, task_id: Optional[str] = None) -> Tuple[str, str]: """Create a new benchmark session. diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py index 694b819..bbc5380 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -26,7 +26,7 @@ logging.getLogger("httpcore").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) -logging.getLogger("mcp.client.streamable_http").setLevel(logging.INFO) +logging.getLogger("mcp.client.streamable_http").setLevel(logging.WARNING) class SessionResult: @@ -246,14 +246,19 @@ def run(self) -> int: try: self.initialize() + # Get list of all available task IDs + logger.info("Fetching available task IDs from Exgentic MCP server") + task_ids = self.exgentic.get_task_ids() + logger.info(f"Found {len(task_ids)} tasks to process") + # Process sessions sequentially - for session_data in self.exgentic.iterate_sessions(): + for session_data in self.exgentic.iterate_sessions(task_ids): # Record session creation time creation_time_ms = (time.time() - session_data.created_at) * 1000 # Note: We can't easily add this to a span since it's created before the span # but we can log it logger.debug(f"Session creation took {creation_time_ms:.2f}ms") - + result = self.process_session(session_data) self.summary.add_result(result) @@ -342,6 +347,7 @@ def main() -> int: try: # Load configuration from environment config = Config.from_env() + logger.info(f"Configuration loaded: {config}") # Create and run runner = Runner(config) From 0c8e92ec3cbf05deb01c4a8067494907ef5ccc90 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Wed, 18 Mar 2026 10:17:17 +0200 Subject: [PATCH 03/17] chore: Remove attribution comments from code Remove all '# Made with ...' comments from Python files for cleaner code. Signed-off-by: Yoav Katz --- exgentic_a2a_runner/exgentic_a2a_runner/__init__.py | 1 - .../exgentic_a2a_runner/a2a_client.py | 1 - exgentic_a2a_runner/exgentic_a2a_runner/config.py | 1 - .../exgentic_a2a_runner/exgentic_adapter.py | 1 - .../exgentic_a2a_runner/mcp_client.py | 13 ++----------- exgentic_a2a_runner/exgentic_a2a_runner/otel.py | 1 - exgentic_a2a_runner/exgentic_a2a_runner/prompt.py | 1 - exgentic_a2a_runner/exgentic_a2a_runner/runner.py | 1 - 8 files changed, 2 insertions(+), 18 deletions(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py b/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py index c1f4801..5916f88 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/__init__.py @@ -6,4 +6,3 @@ __version__ = "0.1.0" -# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py index 1563df4..a3a2455 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py @@ -331,4 +331,3 @@ def send_prompt( raise -# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/config.py b/exgentic_a2a_runner/exgentic_a2a_runner/config.py index b9054eb..944243b 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/config.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/config.py @@ -139,4 +139,3 @@ def from_env(cls) -> "Config": ) -# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py index e888957..9997ad2 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py @@ -185,4 +185,3 @@ def iterate_sessions(self, task_ids: list[str]) -> Iterator[SessionData]: raise -# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py index 2fc7d9d..987b50f 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py @@ -96,7 +96,7 @@ def list_tasks(self) -> list[str]: logger.error(f"Failed to list tasks: {e}") raise RuntimeError(f"Task listing failed: {e}") - def create_session(self, task_id: Optional[str] = None) -> Tuple[str, str]: + def create_session(self, task_id: str) -> Tuple[str, str]: """Create a new benchmark session. Args: @@ -114,15 +114,7 @@ def create_session(self, task_id: Optional[str] = None) -> Tuple[str, str]: logger.info("Creating new benchmark session") try: - # If no task_id provided, list tasks and use the first one - if task_id is None: - tasks = asyncio.run(self._async_list_tasks()) - if not tasks: - raise RuntimeError("No tasks available") - # tasks is a list of task ID strings, not dicts - task_id = tasks[0] - logger.info(f"Using first available task: {task_id}") - + # At this point task_id is guaranteed to be a string assert task_id is not None, "task_id should not be None" result = asyncio.run(self._async_create_session(task_id)) @@ -267,4 +259,3 @@ async def _async_close_session(self, session_id: str) -> None: arguments={"session_id": session_id} ) -# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/otel.py b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py index 395f1df..bd60fa2 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/otel.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py @@ -369,4 +369,3 @@ def record_session_creation(self, span: trace.Span, duration_ms: float) -> None: self.session_creation_latency_histogram.record(duration_ms) -# Made with Bob diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py index 6503540..7bf609a 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py @@ -28,4 +28,3 @@ def build_prompt(task: str, session_id: str) -> str: return prompt -# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py index bbc5380..ad3ad2d 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -368,4 +368,3 @@ def main() -> int: sys.exit(main()) -# Made with Bob \ No newline at end of file From 33ac552dee45170ec154d3305d5c377cfe8fa079 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Wed, 18 Mar 2026 10:26:04 +0200 Subject: [PATCH 04/17] fix: Always use configured A2A_BASE_URL instead of agent card URL The agent card may advertise an internal URL (e.g., 0.0.0.0:8000) that is not accessible from outside the pod. This change ensures we always use the configured A2A_BASE_URL (e.g., localhost:8080 via port-forward) instead of the URL from the agent card. This fixes the 404 error when connecting to agents behind port-forwards or proxies. Signed-off-by: Yoav Katz --- .../exgentic_a2a_runner/a2a_client.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py index a3a2455..8021590 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py @@ -84,26 +84,27 @@ def _get_agent_card(self) -> Dict[str, Any]: def _discover_rpc_url(self) -> str: """Discover the JSON-RPC endpoint URL from the agent card. + Always uses the configured base_url to build the RPC URL, ignoring + the URL from the agent card. This ensures we use the correct URL + when port-forwarding or proxying. + Returns: JSON-RPC endpoint URL """ try: + # Fetch agent card for validation, but don't use its URL card = self._get_agent_card() service_url = card.get("url") - - # If the card has an explicit non-root path, treat it as the RPC URL. - # Otherwise build URL from base + configured endpoint path. + if service_url: - parsed = urlparse(service_url) - if parsed.path and parsed.path != "/": - rpc_url = service_url.rstrip("/") - else: - rpc_url = self._build_rpc_url(service_url) - else: - rpc_url = self._build_rpc_url(self.config.base_url) - - logger.debug(f"Discovered RPC URL from agent card: {rpc_url}") + logger.debug(f"Agent card advertises URL: {service_url}") + logger.debug(f"Using configured base_url instead: {self.config.base_url}") + + # Always build RPC URL from configured base_url + endpoint path + rpc_url = self._build_rpc_url(self.config.base_url) + logger.debug(f"Using RPC URL: {rpc_url}") return rpc_url + except Exception as e: # Fallback to configured base_url + endpoint path if card fetch fails logger.warning(f"Could not fetch agent card, using configured endpoint: {e}") From b1a0201cc32020b71613015d67e1231c1194e343 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Thu, 19 Mar 2026 18:09:20 +0200 Subject: [PATCH 05/17] fix: Debug and enhance Exgentic A2A runner - Fix syntax errors in run-with-port-forward.sh: * Add missing comment symbol on line 36 * Fix unclosed quote on line 40 * Replace parentheses in echo statements to avoid syntax errors * Update service names to match actual cluster services - Configure A2A endpoint to use root path (/) instead of /v1/chat - Enable OTEL trace collection to local Jaeger instance (localhost:4317) - Enhance OTEL instrumentation: * Add full prompt text to span attributes (prompt.text) * Add full response text to span attributes (response.text) * Improve visibility of inputs/outputs in Jaeger traces - Improve prompt instructions: * Add explicit instruction to call submit MCP tool when asked - Enhance logging: * Add evaluation result details to session evaluation logs Signed-off-by: Yoav Katz --- .../exgentic_a2a_runner/exgentic_adapter.py | 2 +- .../exgentic_a2a_runner/otel.py | 2 ++ .../exgentic_a2a_runner/prompt.py | 4 ++- exgentic_a2a_runner/run-with-port-forward.sh | 36 ++++++++++--------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py index 9997ad2..7693572 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py @@ -105,7 +105,7 @@ def evaluate_session(self, session_id: str) -> bool: try: result = self.mcp_client.evaluate_session(session_id) success = result.get("success", False) - logger.info(f"Session {session_id} evaluation: {'success' if success else 'failed'}") + logger.info(f"Session {session_id} evaluation: {'success' if success else 'failed'} {result}") return success except Exception as e: diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/otel.py b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py index bd60fa2..92539c0 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/otel.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/otel.py @@ -266,6 +266,7 @@ def record_prompt(self, span: trace.Span, prompt: str) -> None: """ prompt_chars = len(prompt) span.set_attribute("prompt.chars", prompt_chars) + span.set_attribute("prompt.text", prompt) span.add_event("prompt_built") if self.prompt_size_histogram: @@ -296,6 +297,7 @@ def record_response(self, span: trace.Span, response: str) -> None: """ response_chars = len(response) span.set_attribute("response.chars", response_chars) + span.set_attribute("response.text", response) if self.response_size_histogram: self.response_size_histogram.record(response_chars) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py index 7bf609a..580e40a 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/prompt.py @@ -23,7 +23,9 @@ def build_prompt(task: str, session_id: str) -> str: IMPORTANT: Use session id "{session_id}" in all your interactions with the benchmark tools. -When calling any benchmark-related tools or APIs, you MUST include the session_id parameter with the value "{session_id}". This ensures your actions are properly tracked and evaluated within the correct benchmark session.""" +When calling any benchmark-related tools or APIs, you MUST include the session_id parameter with the value "{session_id}". This ensures your actions are properly tracked and evaluated within the correct benchmark session. + +If you are asked to submit an answer, make sure you call the submit MCP tool.""" return prompt diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh index 8fd4a13..1f34f60 100755 --- a/exgentic_a2a_runner/run-with-port-forward.sh +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -27,25 +27,27 @@ if [ "$CURRENT_CONTEXT" != "kind-kagenti" ]; then fi fi +AGENT_SERVICE=generic-agent-internal-gsm8k +BENCHMARK_SERVICE=exgentic-mcp-gsm8k-mcp echo "" echo "Setting up port forwarding..." -echo " - MCP Server: localhost:8000 -> exgentic-mcp-tau2-mcp.team1:8000" -echo " - A2A Agent: localhost:8080 -> generic-agent2.team1:8080" +echo " - MCP Server: localhost:8000 -> $BENCHMARK_SERVICE.team1:8000" +echo " - A2A Agent: localhost:8080 -> $AGENT_SERVICE.team1:8080" echo "" # Kill any existing port-forwards on these ports echo "Cleaning up existing port-forwards..." -pkill -f "port-forward.*exgentic-mcp-tau2-mcp" 2>/dev/null || true -pkill -f "port-forward.*generic-agent2" 2>/dev/null || true +pkill -f "port-forward.*$BENCHMARK_SERVICE" 2>/dev/null || true +pkill -f "port-forward.*$AGENT_SERVICE" 2>/dev/null || true sleep 2 # Start port forwarding in background echo "Starting port-forward for MCP server..." -kubectl port-forward -n team1 svc/exgentic-mcp-tau2-mcp 8000:8000 & +kubectl port-forward -n team1 svc/$BENCHMARK_SERVICE 8000:8000 & PF_MCP_PID=$! echo "Starting port-forward for A2A agent..." -kubectl port-forward -n team1 svc/generic-agent2 8080:8080 & +kubectl port-forward -n team1 svc/$AGENT_SERVICE 8080:8080 & PF_AGENT_PID=$! # Wait for port forwards to be ready @@ -71,15 +73,15 @@ echo " A2A Agent PID: $PF_AGENT_PID" echo "" # Function to cleanup on exit -cleanup() { - echo "" - echo "Cleaning up port forwards..." - kill $PF_MCP_PID 2>/dev/null || true - kill $PF_AGENT_PID 2>/dev/null || true - echo "Done." -} - -trap cleanup EXIT INT TERM +#cleanup() { +# echo "" +# echo "Cleaning up port forwards..." +# kill $PF_MCP_PID 2>/dev/null || true +# kill $PF_AGENT_PID 2>/dev/null || true +# echo "Done." +#} +# +#trap cleanup EXIT INT TERM # Test connectivity echo "Testing connectivity..." @@ -87,14 +89,14 @@ echo -n " MCP Server: " if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null | grep -q "200\|404"; then echo "✓ Reachable" else - echo "⚠ May not be reachable (this might be OK if no /health endpoint)" + echo "⚠ May not be reachable - this might be OK if no /health endpoint" fi echo -n " A2A Agent: " if curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/.well-known/agent-card.json 2>/dev/null | grep -q "200\|404"; then echo "✓ Reachable" else - echo "⚠ May not be reachable (this might be OK)" + echo "⚠ May not be reachable - this might be OK" fi echo "" From a04c2c10de5b0c3c9f7912f413c98e3d389a8de3 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Thu, 19 Mar 2026 18:22:39 +0200 Subject: [PATCH 06/17] refactor: Move service names to environment configuration - Add AGENT_SERVICE and BENCHMARK_SERVICE to example.env - Update run-with-port-forward.sh to read service names from .env - Use default values if environment variables are not set - Improves configurability and makes it easier to switch between different deployments Signed-off-by: Yoav Katz --- exgentic_a2a_runner/example.env | 10 ++++++++-- exgentic_a2a_runner/run-with-port-forward.sh | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/exgentic_a2a_runner/example.env b/exgentic_a2a_runner/example.env index 25cd0db..75cde90 100644 --- a/exgentic_a2a_runner/example.env +++ b/exgentic_a2a_runner/example.env @@ -5,13 +5,19 @@ # REQUIRED CONFIGURATION # ============================================================ +# Kubernetes service names for port forwarding (used by run-with-port-forward.sh) +# The A2A agent service name in the cluster +AGENT_SERVICE=generic-agent-internal +# The Exgentic MCP benchmark service name in the cluster +BENCHMARK_SERVICE=exgentic-mcp-gsm8k-mcp + # Exgentic MCP Server URL (REQUIRED) # The endpoint for the Exgentic MCP server that provides benchmark tasks -EXGENTIC_MCP_SERVER_URL=http://localhost:3000 +EXGENTIC_MCP_SERVER_URL=http://localhost:8000/mcp # A2A endpoint base URL (REQUIRED) # The Kagenti agent endpoint that will execute the tasks -A2A_BASE_URL=http://localhost:8000 +A2A_BASE_URL=http://localhost:8080 # ============================================================ # EXGENTIC CONFIGURATION (Optional) diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh index 1f34f60..9b30f25 100755 --- a/exgentic_a2a_runner/run-with-port-forward.sh +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -27,8 +27,11 @@ if [ "$CURRENT_CONTEXT" != "kind-kagenti" ]; then fi fi -AGENT_SERVICE=generic-agent-internal-gsm8k -BENCHMARK_SERVICE=exgentic-mcp-gsm8k-mcp +# Load environment variables if .env exists +if [ -f "$(dirname "$0")/.env" ]; then + source "$(dirname "$0")/.env" +fi + echo "" echo "Setting up port forwarding..." echo " - MCP Server: localhost:8000 -> $BENCHMARK_SERVICE.team1:8000" From a262ec66fdb33b7658e30435192ef885f7b0628f Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 09:07:10 +0200 Subject: [PATCH 07/17] feat: Add parallel session processing support - Add MAX_PARALLEL_SESSIONS configuration parameter (default: 1) - Implement ThreadPoolExecutor for concurrent session execution - Add thread-safe result collection with mutex lock - Display max parallel sessions in run summary - Maintain backward compatibility with sequential processing (max_parallel_sessions=1) - Support abort_on_failure in parallel mode by canceling remaining futures Benefits: - Significantly improves throughput for I/O-bound workloads - Allows users to configure parallelism based on their needs - Maintains all existing functionality and error handling Signed-off-by: Yoav Katz --- exgentic_a2a_runner/example.env | 5 ++ .../exgentic_a2a_runner/config.py | 2 + .../exgentic_a2a_runner/runner.py | 90 +++++++++++++++---- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/exgentic_a2a_runner/example.env b/exgentic_a2a_runner/example.env index 75cde90..c78ee9d 100644 --- a/exgentic_a2a_runner/example.env +++ b/exgentic_a2a_runner/example.env @@ -29,6 +29,11 @@ EXGENTIC_MCP_TIMEOUT_SECONDS=60 # Maximum number of tasks/sessions to process (useful for testing) MAX_TASKS=10 +# Maximum number of parallel sessions to run concurrently (default: 1) +# Set to 1 for sequential processing, higher values for parallel execution +# Example: MAX_PARALLEL_SESSIONS=5 will run up to 5 sessions simultaneously +MAX_PARALLEL_SESSIONS=1 + # Stop processing on first failure (default: false) ABORT_ON_FAILURE=false diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/config.py b/exgentic_a2a_runner/exgentic_a2a_runner/config.py index 944243b..a2a230b 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/config.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/config.py @@ -37,6 +37,7 @@ class ExgenticConfig: mcp_timeout_seconds: int = 60 max_tasks: Optional[int] = None abort_on_failure: bool = False + max_parallel_sessions: int = 1 @classmethod def from_env(cls) -> "ExgenticConfig": @@ -50,6 +51,7 @@ def from_env(cls) -> "ExgenticConfig": mcp_timeout_seconds=_get_int("EXGENTIC_MCP_TIMEOUT_SECONDS", 60) or 60, max_tasks=_get_int("MAX_TASKS"), abort_on_failure=_get_bool("ABORT_ON_FAILURE", False), + max_parallel_sessions=_get_int("MAX_PARALLEL_SESSIONS", 1) or 1, ) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py index ad3ad2d..1dff9b8 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -6,7 +6,9 @@ import argparse import logging import sys +import threading import time +from concurrent.futures import ThreadPoolExecutor, as_completed from typing import List, Optional from .a2a_client import A2AProxyClient @@ -55,10 +57,12 @@ class RunSummary: def __init__(self): self.start_time = time.time() self.results: List[SessionResult] = [] + self._lock = threading.Lock() def add_result(self, result: SessionResult) -> None: - """Add a session result.""" - self.results.append(result) + """Add a session result (thread-safe).""" + with self._lock: + self.results.append(result) def get_summary(self) -> dict: """Get summary statistics.""" @@ -92,13 +96,18 @@ def get_summary(self) -> dict: "p95_latency_ms": p95, } - def print_summary(self) -> None: - """Print summary to console.""" + def print_summary(self, max_parallel_sessions: int = 1) -> None: + """Print summary to console. + + Args: + max_parallel_sessions: Maximum number of parallel sessions configured + """ summary = self.get_summary() print("\n" + "=" * 60) print("RUN SUMMARY") print("=" * 60) + print(f"Max Parallel Sessions: {max_parallel_sessions}") print(f"Sessions Attempted: {summary['sessions_attempted']}") print(f"Sessions Succeeded: {summary['sessions_succeeded']}") print(f"Sessions Failed: {summary['sessions_failed']}") @@ -124,6 +133,7 @@ def __init__(self, config: Config): self.a2a_client = A2AProxyClient(config.a2a) self.otel = OTELInstrumentation(config.otel) self.summary = RunSummary() + self.max_parallel_sessions = config.exgentic.max_parallel_sessions def initialize(self) -> None: """Initialize all components.""" @@ -251,24 +261,66 @@ def run(self) -> int: task_ids = self.exgentic.get_task_ids() logger.info(f"Found {len(task_ids)} tasks to process") - # Process sessions sequentially - for session_data in self.exgentic.iterate_sessions(task_ids): - # Record session creation time - creation_time_ms = (time.time() - session_data.created_at) * 1000 - # Note: We can't easily add this to a span since it's created before the span - # but we can log it - logger.debug(f"Session creation took {creation_time_ms:.2f}ms") + max_workers = self.config.exgentic.max_parallel_sessions + + if max_workers == 1: + # Sequential processing (original behavior) + logger.info("Processing sessions sequentially") + for session_data in self.exgentic.iterate_sessions(task_ids): + # Record session creation time + creation_time_ms = (time.time() - session_data.created_at) * 1000 + logger.debug(f"Session creation took {creation_time_ms:.2f}ms") + + result = self.process_session(session_data) + self.summary.add_result(result) + + # Check abort on failure + if not result.success and self.config.exgentic.abort_on_failure: + logger.error("Aborting due to session failure (ABORT_ON_FAILURE=true)") + break + else: + # Parallel processing + logger.info(f"Processing sessions in parallel with {max_workers} workers") - result = self.process_session(session_data) - self.summary.add_result(result) - - # Check abort on failure - if not result.success and self.config.exgentic.abort_on_failure: - logger.error("Aborting due to session failure (ABORT_ON_FAILURE=true)") - break + # Collect all session data first + sessions = list(self.exgentic.iterate_sessions(task_ids)) + + # Process sessions in parallel + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all sessions + future_to_session = { + executor.submit(self.process_session, session_data): session_data + for session_data in sessions + } + + # Process results as they complete + for future in as_completed(future_to_session): + session_data = future_to_session[future] + try: + result = future.result() + self.summary.add_result(result) + + # Check abort on failure + if not result.success and self.config.exgentic.abort_on_failure: + logger.error("Aborting due to session failure (ABORT_ON_FAILURE=true)") + # Cancel remaining futures + for f in future_to_session: + f.cancel() + break + except Exception as e: + logger.error(f"Session {session_data.session_id} raised exception: {e}") + # Create a failure result + result = SessionResult( + session_id=session_data.session_id, + success=False, + latency_ms=0, + evaluation_result=False, + error=str(e), + ) + self.summary.add_result(result) # Print summary - self.summary.print_summary() + self.summary.print_summary(max_parallel_sessions=self.max_parallel_sessions) # Return success if at least one session succeeded if any(r.success for r in self.summary.results): From 78e0aaccd89419b50441c8802f63cba502f48734 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 10:09:34 +0200 Subject: [PATCH 08/17] feat: Add failed sessions error table to summary - Display table of all failed sessions with their error messages at end of run summary - Truncate long error messages to 50 characters for readability - Only show table if there are failed sessions - Helps quickly identify and diagnose session failures Signed-off-by: Yoav Katz --- .../exgentic_a2a_runner/runner.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py index 1dff9b8..c38b0c1 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -116,7 +116,23 @@ def print_summary(self, max_parallel_sessions: int = 1) -> None: print(f"Average Latency: {summary['average_latency_ms']:.2f}ms") print(f"P50 Latency: {summary['p50_latency_ms']:.2f}ms") print(f"P95 Latency: {summary['p95_latency_ms']:.2f}ms") - print("=" * 60 + "\n") + print("=" * 60) + + # Print error table if there are any failures + failed_results = [r for r in self.results if not r.success and r.error] + if failed_results: + print("\nFAILED SESSIONS") + print("=" * 60) + print(f"{'Session ID':<40} {'Error':<20}") + print("-" * 60) + for result in failed_results: + # Truncate error message if too long + error = result.error or "Unknown error" + error_msg = error[:100] + "..." if len(error) > 100 else error + print(f"{result.session_id:<40} {error_msg:<20}") + print("=" * 60) + + print() class Runner: From d97497060cc2293e311d662a48d1b31116b416a7 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 10:38:30 +0200 Subject: [PATCH 09/17] fix: Extract all task data before handling terminal states - Extract text from artifacts and result first, regardless of state - Then handle failed/canceled/rejected states with extracted information - Include extracted output in error messages for better debugging - Provides complete context when tasks don't complete successfully Signed-off-by: Yoav Katz --- .../exgentic_a2a_runner/a2a_client.py | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py index 8021590..0e8d655 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/a2a_client.py @@ -201,18 +201,9 @@ def _extract_text_from_task(self, task: Dict[str, Any]) -> str: """ status = task.get("status", {}) state = status.get("state") + extracted_text = None - if state == "failed": - error = status.get("error", "Unknown error") - raise ValueError(f"Task failed: {error}") - - if state == "canceled": - raise ValueError("Task was canceled") - - if state == "rejected": - raise ValueError("Task was rejected") - - # Look for artifacts in task (A2A spec) + # First, try to extract all possible data from artifacts (A2A spec) if "artifacts" in task: artifacts = task["artifacts"] if isinstance(artifacts, list) and len(artifacts) > 0: @@ -227,22 +218,49 @@ def _extract_text_from_task(self, task: Dict[str, Any]) -> str: if text: text_parts.append(text) if text_parts: - return "\n".join(text_parts) + extracted_text = "\n".join(text_parts) - # Look for result in task (fallback) - if "result" in task: + # If no artifacts, look for result in task (fallback) + if not extracted_text and "result" in task: result = task["result"] if isinstance(result, dict): # Try to extract message from result if "message" in result: - return self._extract_text_from_message(result["message"]) + try: + extracted_text = self._extract_text_from_message(result["message"]) + except ValueError: + pass # Try direct text extraction - if "text" in result: - return str(result["text"]) - if "content" in result: - return str(result["content"]) + if not extracted_text and "text" in result: + extracted_text = str(result["text"]) + if not extracted_text and "content" in result: + extracted_text = str(result["content"]) elif isinstance(result, str): - return result + extracted_text = result + + # Now handle different states with extracted information + if state == "failed": + error = status.get("error", "Unknown error") + if extracted_text: + raise ValueError(f"Task failed: {error}. Output: {extracted_text}") + else: + raise ValueError(f"Task failed: {error}") + + if state == "canceled": + if extracted_text: + raise ValueError(f"Task was canceled. Partial output: {extracted_text}") + else: + raise ValueError("Task was canceled") + + if state == "rejected": + if extracted_text: + raise ValueError(f"Task was rejected. Output: {extracted_text}") + else: + raise ValueError("Task was rejected") + + # For completed tasks, return the extracted text + if extracted_text: + return extracted_text raise ValueError("Could not extract text from task result") From bae73c3930ef317f611e07ea54c4ffb6398d2671 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 12:33:07 +0200 Subject: [PATCH 10/17] feat(exgentic): Add deployment and configuration scripts for Kagenti cluster Add three new scripts to automate deployment and configuration of Exgentic benchmark system on Kagenti Kubernetes cluster: 1. deploy-benchmark.sh: Deploy MCP tools via Kagenti API - Syncs local container images to cluster registry - Authenticates with Keycloak using password grant flow - Deploys tools with proper service configuration - Patches imagePullPolicy for local images - Waits for deployment readiness 2. deploy-agent.sh: Deploy A2A agents from source - Fetches and parses environment variables from GitHub - Deploys agents using Shipwright builds - Monitors build progress and waits for completion - Waits for deployment creation and readiness - Tests agent accessibility via A2A protocol - Fixes port configuration (8080 -> 8000) 3. configure-agent-environment.sh: Configure agent environment - Updates OpenAI API secret via kubectl patch - Patches agent deployment with Azure OpenAI settings - Accepts benchmark name as parameter - Waits for rollout completion These scripts enable automated deployment and testing of the Exgentic benchmark system without manual kubectl commands or UI interaction. Fixes: - Agent port mismatch (container port 8000 vs service port 8080) - MCP_URLS environment variable configuration - Azure OpenAI endpoint and model configuration Signed-off-by: Yoav Katz --- .../configure-agent-environment.sh | 103 ++++++ exgentic_a2a_runner/deploy-agent.sh | 287 ++++++++++++++++ exgentic_a2a_runner/deploy-benchmark.sh | 318 ++++++++++++++++++ 3 files changed, 708 insertions(+) create mode 100644 exgentic_a2a_runner/configure-agent-environment.sh create mode 100755 exgentic_a2a_runner/deploy-agent.sh create mode 100755 exgentic_a2a_runner/deploy-benchmark.sh diff --git a/exgentic_a2a_runner/configure-agent-environment.sh b/exgentic_a2a_runner/configure-agent-environment.sh new file mode 100644 index 0000000..e0e92eb --- /dev/null +++ b/exgentic_a2a_runner/configure-agent-environment.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Configure agent environment settings +# Usage: ./configure-agent-environment.sh +# Example: ./configure-agent-environment.sh gsm8k +# This script updates the Kubernetes secret and patches the agent deployment + +set -e + +BENCHMARK_NAME="$1" + +if [ -z "$BENCHMARK_NAME" ]; then + echo "Error: Benchmark name is required" + echo "Usage: $0 " + echo "Example: $0 gsm8k" + exit 1 +fi + +NAMESPACE="team1" +AGENT_NAME="generic-agent-internal-${BENCHMARK_NAME}" + +echo "==========================================" +echo "Configuring Agent Environment: $AGENT_NAME" +echo "==========================================" +echo "" + +# Step 1: Update the openai-secret with current OPENAI_API_KEY +echo "Step 1: Updating openai-secret with OPENAI_API_KEY..." + +if [ -z "$OPENAI_API_KEY" ]; then + echo "Error: OPENAI_API_KEY environment variable is not set" + exit 1 +fi + +# Encode the API key in base64 +ENCODED_KEY=$(echo -n "$OPENAI_API_KEY" | base64) + +# Patch the secret +kubectl patch secret openai-secret -n $NAMESPACE --type='json' -p="[ + { + \"op\": \"replace\", + \"path\": \"/data/apikey\", + \"value\": \"$ENCODED_KEY\" + } +]" + +echo "✓ Secret updated" +echo "" + +# Step 2: Patch agent deployment with Azure OpenAI settings +echo "Step 2: Patching agent deployment with Azure OpenAI settings..." + +if [ -z "$OPENAI_API_BASE" ]; then + echo "Error: OPENAI_API_BASE environment variable is not set" + exit 1 +fi + +# Get current env vars +CURRENT_ENV=$(kubectl get deployment $AGENT_NAME -n $NAMESPACE -o json | jq '.spec.template.spec.containers[0].env') + +# Find indices of the env vars we need to update +LLM_API_BASE_INDEX=$(echo "$CURRENT_ENV" | jq 'map(.name == "LLM_API_BASE") | index(true)') +LLM_MODEL_INDEX=$(echo "$CURRENT_ENV" | jq 'map(.name == "LLM_MODEL") | index(true)') + +if [ "$LLM_API_BASE_INDEX" = "null" ] || [ "$LLM_MODEL_INDEX" = "null" ]; then + echo "Error: Could not find LLM_API_BASE or LLM_MODEL in deployment" + exit 1 +fi + +# Patch only the specific env vars +kubectl patch deployment $AGENT_NAME -n $NAMESPACE --type='json' -p="[ + { + \"op\": \"replace\", + \"path\": \"/spec/template/spec/containers/0/env/$LLM_API_BASE_INDEX/value\", + \"value\": \"$OPENAI_API_BASE\" + }, + { + \"op\": \"replace\", + \"path\": \"/spec/template/spec/containers/0/env/$LLM_MODEL_INDEX/value\", + \"value\": \"Azure/gpt-4o\" + } +]" + +echo "✓ Deployment patched" +echo "" + +# Step 3: Wait for rollout +echo "Step 3: Waiting for deployment rollout..." +kubectl rollout status deployment/$AGENT_NAME -n $NAMESPACE --timeout=120s + +echo "✓ Rollout complete" +echo "" + +echo "==========================================" +echo "Configuration Complete!" +echo "==========================================" +echo "" +echo "Settings applied:" +echo " LLM_API_BASE: $OPENAI_API_BASE" +echo " LLM_MODEL: Azure/gpt-4o" +echo " OPENAI_API_KEY: (from secret)" +echo "" + +# Made with Bob diff --git a/exgentic_a2a_runner/deploy-agent.sh b/exgentic_a2a_runner/deploy-agent.sh new file mode 100755 index 0000000..b7c016d --- /dev/null +++ b/exgentic_a2a_runner/deploy-agent.sh @@ -0,0 +1,287 @@ +#!/bin/bash +# Deploy generic agent to Kagenti cluster via API +# Usage: ./deploy-agent.sh +# Example: ./deploy-agent.sh gsm8k admin admin + +set -e + +BENCHMARK_NAME="$1" +KEYCLOAK_USERNAME="${2:-admin}" +KEYCLOAK_PASSWORD="${3:-admin}" + +if [ -z "$BENCHMARK_NAME" ]; then + echo "Error: Benchmark name is required" + echo "Usage: $0 [keycloak-username] [keycloak-password]" + echo "Example: $0 gsm8k admin admin" + exit 1 +fi + +AGENT_NAME="generic-agent-internal-${BENCHMARK_NAME}" +TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}-mcp" +NAMESPACE="team1" +KAGENTI_API="http://localhost:8001" +KAGENTI_PORT=8001 +KEYCLOAK_API="http://localhost:8002" +KEYCLOAK_PORT=8002 + +echo "==========================================" +echo "Deploying Generic Agent: $AGENT_NAME" +echo "==========================================" +echo "" + +# Step 1: Set up port-forward to Keycloak +echo "Step 1: Setting up port-forward to Keycloak..." +if lsof -Pi :$KEYCLOAK_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "✓ Port $KEYCLOAK_PORT is already in use (assuming Keycloak is accessible)" +else + echo "Starting port-forward to keycloak on port $KEYCLOAK_PORT..." + kubectl port-forward -n keycloak svc/keycloak-service $KEYCLOAK_PORT:8080 >/dev/null 2>&1 & + KEYCLOAK_PF_PID=$! + sleep 2 +fi + +echo "" + +# Step 2: Get Keycloak authentication token +echo "Step 2: Getting Keycloak authentication token..." +TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/kagenti/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$KEYCLOAK_USERNAME" \ + -d "password=$KEYCLOAK_PASSWORD" \ + -d "grant_type=password" \ + -d "client_id=kagenti" || echo "TOKEN_ERROR") + +if [ "$TOKEN_RESPONSE" = "TOKEN_ERROR" ]; then + echo "Error: Failed to get authentication token from Keycloak" + exit 1 +fi + +ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed 's/"access_token":"\([^"]*\)"/\1/') + +if [ -z "$ACCESS_TOKEN" ]; then + echo "Error: Failed to extract access token" + exit 1 +fi + +echo "✓ Successfully obtained authentication token" + +echo "" + +# Step 3: Set up port-forward to Kagenti backend +echo "Step 3: Setting up port-forward to Kagenti backend..." +if lsof -Pi :$KAGENTI_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "✓ Port $KAGENTI_PORT is already in use (assuming Kagenti backend is accessible)" +else + echo "Starting port-forward to kagenti-backend on port $KAGENTI_PORT..." + kubectl port-forward -n kagenti-system svc/kagenti-backend $KAGENTI_PORT:8000 >/dev/null 2>&1 & + PORT_FORWARD_PID=$! + sleep 2 +fi + +echo "" + +# Step 4: Delete existing agent if it exists +echo "Step 4: Deleting existing agent via Kagenti API if it exists..." +DELETE_RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/kagenti_delete_agent_response.txt -X DELETE "$KAGENTI_API/api/v1/agents/$NAMESPACE/$AGENT_NAME" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + +if [ "$DELETE_RESPONSE" = "200" ] || [ "$DELETE_RESPONSE" = "404" ]; then + echo "✓ Agent deleted or did not exist (HTTP $DELETE_RESPONSE)" +else + echo "Warning: Delete returned HTTP $DELETE_RESPONSE" +fi + +sleep 3 + +echo "" + +# Step 5: Fetch and parse environment variables +echo "Step 5: Fetching environment variables..." +ENV_CONTENT=$(curl -s https://raw.githubusercontent.com/kagenti/agent-examples/refs/heads/main/a2a/generic_agent/.env.openai) + +# Parse env vars using the Kagenti API +ENV_PARSE_RESPONSE=$(curl -s -X POST "$KAGENTI_API/api/v1/agents/parse-env" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -d "{\"content\": $(echo "$ENV_CONTENT" | jq -Rs .)}") + +ENV_VARS=$(echo "$ENV_PARSE_RESPONSE" | jq '.envVars') + +echo "✓ Environment variables parsed" + +echo "" + +# Step 6: Deploy agent via Kagenti API +echo "Step 6: Deploying agent via Kagenti API..." + +# Add MCP_URLS to environment variables +MCP_URL="http://${TOOL_NAME}-mcp:8000/mcp" +ENV_VARS_WITH_MCP=$(echo "$ENV_VARS" | jq ". + [{\"name\": \"MCP_URLS\", \"value\": \"$MCP_URL\"}]") + +AGENT_JSON=$(cat </dev/null || echo "Unknown") + BUILD_REASON=$(kubectl get buildrun "$BUILD_RUN_NAME" -n "$NAMESPACE" -o jsonpath='{.status.conditions[?(@.type=="Succeeded")].reason}' 2>/dev/null || echo "Unknown") + + if [ "$BUILD_STATUS" = "True" ]; then + echo "✓ Build completed successfully" + break + elif [ "$BUILD_STATUS" = "False" ]; then + echo "✗ Build failed with reason: $BUILD_REASON" + echo "Check logs with: kubectl logs -n $NAMESPACE -l buildrun.shipwright.io/name=$BUILD_RUN_NAME" + exit 1 + fi + + echo " Build in progress... ($i/60)" + sleep 5 + done + + if [ "$BUILD_STATUS" != "True" ]; then + echo "✗ Build did not complete within 5 minutes" + exit 1 + fi +fi + +echo "" + +# Step 8: Wait for agent deployment to be created and ready +echo "Step 8: Waiting for agent deployment to be created..." + +# Wait for deployment to be created (up to 2 minutes) +for i in {1..24}; do + if kubectl get deployment $AGENT_NAME -n $NAMESPACE >/dev/null 2>&1; then + echo "✓ Agent deployment created" + break + fi + echo " Waiting for deployment to be created... ($i/24)" + sleep 5 +done + +# Check if deployment exists +if ! kubectl get deployment $AGENT_NAME -n $NAMESPACE >/dev/null 2>&1; then + echo "✗ Agent deployment was not created within 2 minutes" + exit 1 +fi + +echo "Waiting for agent deployment to be ready..." +kubectl wait --for=condition=available deployment/$AGENT_NAME -n $NAMESPACE --timeout=120s + +if [ $? -ne 0 ]; then + echo "✗ Agent deployment did not become ready" + exit 1 +fi + +echo "✓ Agent deployment is ready" +echo "" + +# Step 9: Test agent card access +echo "Step 9: Testing agent card access..." + +# Set up port-forward to agent +AGENT_PORT=8084 +kubectl port-forward -n $NAMESPACE svc/$AGENT_NAME $AGENT_PORT:8080 >/dev/null 2>&1 & +AGENT_PF_PID=$! +sleep 2 + +# Test agent card endpoint (trying common A2A methods) +CARD_RESPONSE=$(curl -s -X POST http://localhost:$AGENT_PORT/ \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "method": "agent/card", "id": 1}' 2>/dev/null) + +if [ -z "$CARD_RESPONSE" ]; then + echo "✗ No response from agent" + kill $AGENT_PF_PID 2>/dev/null + exit 1 +fi + +# Check if response contains error +if echo "$CARD_RESPONSE" | grep -q '"error"'; then + echo "Agent responded but method may be incorrect:" + echo "$CARD_RESPONSE" | jq '.' 2>/dev/null || echo "$CARD_RESPONSE" + echo "" + echo "Note: Agent is running but may use different A2A method names" +else + echo "✓ Agent card access successful:" + echo "$CARD_RESPONSE" | jq '.' 2>/dev/null || echo "$CARD_RESPONSE" +fi + +# Clean up port-forward +kill $AGENT_PF_PID 2>/dev/null + +echo "" +echo "==========================================" +echo "Deployment Complete!" +echo "==========================================" +echo "" +echo "Agent: $AGENT_NAME.$NAMESPACE:8080" +echo "Tool: $TOOL_NAME.$NAMESPACE:8000" +echo "" +echo "Agent is ready and accessible!" +echo "" + +# Made with Bob diff --git a/exgentic_a2a_runner/deploy-benchmark.sh b/exgentic_a2a_runner/deploy-benchmark.sh new file mode 100755 index 0000000..9393e8e --- /dev/null +++ b/exgentic_a2a_runner/deploy-benchmark.sh @@ -0,0 +1,318 @@ +#!/bin/bash +# Deploy Exgentic benchmark to Kagenti cluster +# Usage: ./deploy-benchmark.sh [keycloak-username] [keycloak-password] +# Example: ./deploy-benchmark.sh gsm8k admin admin + +set -e + +BENCHMARK_NAME="$1" +KEYCLOAK_USERNAME="${2:-admin}" +KEYCLOAK_PASSWORD="${3:-admin}" + +if [ -z "$BENCHMARK_NAME" ]; then + echo "Error: Benchmark name is required" + echo "Usage: $0 [keycloak-username] [keycloak-password]" + echo "Example: $0 gsm8k admin admin" + exit 1 +fi + +IMAGE_NAME="localhost/exgentic-mcp-${BENCHMARK_NAME}:latest" +TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}-mcp" +NAMESPACE="team1" +KAGENTI_API="http://localhost:8001" # Using 8001 to avoid conflict with MCP server on 8000 +KAGENTI_PORT=8001 +KEYCLOAK_API="http://localhost:8002" +KEYCLOAK_PORT=8002 + +echo "==========================================" +echo "Deploying Exgentic Benchmark: $BENCHMARK_NAME" +echo "==========================================" +echo "" + +# Step 1: Check if image exists locally +echo "Step 1: Checking for local image..." +if command -v podman &> /dev/null; then + CONTAINER_CMD="podman" +elif command -v docker &> /dev/null; then + CONTAINER_CMD="docker" +else + echo "Error: Neither podman nor docker found" + exit 1 +fi + +echo "Using container runtime: $CONTAINER_CMD" + +if ! $CONTAINER_CMD image inspect "$IMAGE_NAME" &> /dev/null; then + echo "Error: Image $IMAGE_NAME not found locally" + echo "Please build the image first" + exit 1 +fi + +echo "✓ Image $IMAGE_NAME found locally" +echo "" + +# Step 2: Check if image needs syncing +echo "Step 2: Checking if image sync is needed..." +if ! command -v kind &> /dev/null; then + echo "Error: kind command not found" + exit 1 +fi + +# Get local image ID +LOCAL_IMAGE_ID=$($CONTAINER_CMD inspect "$IMAGE_NAME" --format='{{.Id}}' 2>/dev/null || echo "") + +if [ -z "$LOCAL_IMAGE_ID" ]; then + echo "Error: Could not get local image ID" + exit 1 +fi + +echo "Local image ID: $LOCAL_IMAGE_ID" + +# Get cluster image ID (check if image exists in cluster) +# Use podman if available, otherwise docker +if command -v podman &> /dev/null; then + CLUSTER_IMAGE_ID=$(podman exec kagenti-control-plane crictl inspecti "$IMAGE_NAME" 2>/dev/null | grep '"id":' | head -1 | sed 's/.*"id": *"\([^"]*\)".*/\1/' || echo "") +else + CLUSTER_IMAGE_ID=$(docker exec kagenti-control-plane crictl inspecti "$IMAGE_NAME" 2>/dev/null | grep '"id":' | head -1 | sed 's/.*"id": *"\([^"]*\)".*/\1/' || echo "") +fi + +# Normalize IDs by removing sha256: prefix if present +LOCAL_IMAGE_ID_NORMALIZED="${LOCAL_IMAGE_ID#sha256:}" +CLUSTER_IMAGE_ID_NORMALIZED="${CLUSTER_IMAGE_ID#sha256:}" + +if [ -z "$CLUSTER_IMAGE_ID" ]; then + echo "Image not found in cluster, syncing..." + NEED_SYNC=true +elif [ "$LOCAL_IMAGE_ID_NORMALIZED" != "$CLUSTER_IMAGE_ID_NORMALIZED" ]; then + echo "Cluster image ID: $CLUSTER_IMAGE_ID" + echo "Images differ, syncing..." + NEED_SYNC=true +else + echo "Cluster image ID: $CLUSTER_IMAGE_ID" + echo "✓ Images match, skipping sync" + NEED_SYNC=false +fi + +if [ "$NEED_SYNC" = true ]; then + echo "Saving and loading image..." + $CONTAINER_CMD save "$IMAGE_NAME" | kind load image-archive /dev/stdin --name kagenti + echo "✓ Image synced to kind-kagenti cluster" +fi + +echo "" + +# Step 3: Setting up port-forward to Keycloak... +echo "Step 3: Setting up port-forward to Keycloak..." + +# Check if port-forward is already running +if lsof -Pi :$KEYCLOAK_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "✓ Port $KEYCLOAK_PORT is already in use (assuming Keycloak is accessible)" +else + echo "Starting port-forward to keycloak on port $KEYCLOAK_PORT..." + kubectl port-forward -n keycloak svc/keycloak-service $KEYCLOAK_PORT:8080 >/dev/null 2>&1 & + KEYCLOAK_PF_PID=$! + + # Wait for port-forward to be ready + echo "Waiting for Keycloak port-forward to be ready..." + for i in {1..10}; do + if curl -s $KEYCLOAK_API/health >/dev/null 2>&1; then + echo "✓ Keycloak port-forward is ready" + break + fi + if [ $i -eq 10 ]; then + echo "Warning: Keycloak port-forward may not be ready, continuing anyway..." + fi + sleep 1 + done +fi + +echo "" + +# Step 4: Get Keycloak authentication token... +echo "Step 4: Getting Keycloak authentication token..." + +# Get token from Keycloak using kagenti client (with direct access grants enabled) +TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/kagenti/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$KEYCLOAK_USERNAME" \ + -d "password=$KEYCLOAK_PASSWORD" \ + -d "grant_type=password" \ + -d "client_id=kagenti" || echo "TOKEN_ERROR") + +if [ "$TOKEN_RESPONSE" = "TOKEN_ERROR" ]; then + echo "Error: Failed to get authentication token from Keycloak" + echo "Please check your Keycloak credentials" + exit 1 +fi + +ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed 's/"access_token":"\([^"]*\)"/\1/') + +if [ -z "$ACCESS_TOKEN" ]; then + echo "Error: Failed to extract access token from Keycloak response" + echo "Response: $TOKEN_RESPONSE" + exit 1 +fi + +echo "✓ Successfully obtained authentication token" + +echo "" + +# Step 5: Set up port-forward to Kagenti backend +echo "Step 5: Setting up port-forward to Kagenti backend..." + +# Check if port-forward is already running +if lsof -Pi :$KAGENTI_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "✓ Port $KAGENTI_PORT is already in use (assuming Kagenti backend is accessible)" +else + echo "Starting port-forward to kagenti-backend on port $KAGENTI_PORT..." + kubectl port-forward -n kagenti-system svc/kagenti-backend $KAGENTI_PORT:8000 >/dev/null 2>&1 & + PORT_FORWARD_PID=$! + + # Wait for port-forward to be ready + echo "Waiting for port-forward to be ready..." + for i in {1..10}; do + if curl -s $KAGENTI_API/api/v1/namespaces >/dev/null 2>&1; then + echo "✓ Port-forward is ready" + break + fi + if [ $i -eq 10 ]; then + echo "Error: Port-forward failed to become ready" + kill $PORT_FORWARD_PID 2>/dev/null || true + exit 1 + fi + sleep 1 + done +fi + +echo "" + +# Step 6: Delete existing tool via Kagenti API if it exists +echo "Step 6: Deleting existing tool via Kagenti API if it exists..." +DELETE_RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/kagenti_delete_response.txt -X DELETE "$KAGENTI_API/api/v1/tools/$NAMESPACE/$TOOL_NAME" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + +if [ "$DELETE_RESPONSE" = "200" ] || [ "$DELETE_RESPONSE" = "404" ]; then + echo "✓ Tool deleted or did not exist (HTTP $DELETE_RESPONSE)" +else + echo "Warning: Delete returned HTTP $DELETE_RESPONSE" + cat /tmp/kagenti_delete_response.txt +fi + +# Wait a moment for deletion to complete +sleep 3 + +echo "" + +# Step 7: Deploy tool using Kagenti API +echo "Step 7: Deploying tool via Kagenti API..." + +# Create tool deployment JSON following Kagenti API format +TOOL_JSON=$(cat </dev/null || echo "Warning: Could not patch imagePullPolicy" +echo "✓ ImagePullPolicy patched" + +echo "" + +# Step 9: Wait for tool to be ready +echo "Step 9: Waiting for tool to be ready..." + +MAX_WAIT=120 +WAIT_INTERVAL=5 +ELAPSED=0 + +while [ $ELAPSED -lt $MAX_WAIT ]; do + # Check if pod is running (using Kagenti's label format) + POD_STATUS=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=$TOOL_NAME -o jsonpath='{.items[0].status.phase}' 2>/dev/null || echo "NotFound") + + if [ "$POD_STATUS" = "Running" ]; then + # Check if pod is ready + POD_READY=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=$TOOL_NAME -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "False") + + if [ "$POD_READY" = "True" ]; then + echo "✓ Tool is ready!" + + # Get pod name + POD_NAME=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=$TOOL_NAME -o jsonpath='{.items[0].metadata.name}') + echo "" + echo "Pod: $POD_NAME" + echo "Service: $TOOL_NAME.$NAMESPACE:8000" + echo "" + echo "To access the tool:" + echo " kubectl port-forward -n $NAMESPACE svc/$TOOL_NAME 8000:8000" + echo "" + exit 0 + fi + fi + + echo " Status: $POD_STATUS (waiting...)" + sleep $WAIT_INTERVAL + ELAPSED=$((ELAPSED + WAIT_INTERVAL)) +done + +echo "Error: Tool did not become ready within ${MAX_WAIT}s" +echo "" +echo "Check status with:" +echo " kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=$TOOL_NAME" +echo " kubectl logs -n $NAMESPACE -l app.kubernetes.io/name=$TOOL_NAME" +exit 1 + +# Made with Bob From 8008e76304494fdf0c8b999b550f1a46e1fd93d0 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 13:02:51 +0200 Subject: [PATCH 11/17] fix: change A2A agent port from 8080 to 8081 to avoid conflict with kagenti-ui Port 8080 was being used by both the A2A agent port-forward and the kagenti-ui service (via Istio gateway), causing intermittent access issues to http://kagenti-ui.localtest.me:8080/. Changes: - Updated A2A_BASE_URL from localhost:8080 to localhost:8081 in example.env - Modified run-with-port-forward.sh to forward A2A agent to port 8081 - Updated connectivity test to check port 8081 This allows kagenti-ui to be accessed on port 8080 via Istio gateway while the A2A agent uses port 8081, eliminating port conflicts. Signed-off-by: Yoav Katz --- exgentic_a2a_runner/example.env | 2 +- exgentic_a2a_runner/run-with-port-forward.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exgentic_a2a_runner/example.env b/exgentic_a2a_runner/example.env index c78ee9d..6ba21ce 100644 --- a/exgentic_a2a_runner/example.env +++ b/exgentic_a2a_runner/example.env @@ -17,7 +17,7 @@ EXGENTIC_MCP_SERVER_URL=http://localhost:8000/mcp # A2A endpoint base URL (REQUIRED) # The Kagenti agent endpoint that will execute the tasks -A2A_BASE_URL=http://localhost:8080 +A2A_BASE_URL=http://localhost:8081 # ============================================================ # EXGENTIC CONFIGURATION (Optional) diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh index 9b30f25..e047fcb 100755 --- a/exgentic_a2a_runner/run-with-port-forward.sh +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -35,7 +35,7 @@ fi echo "" echo "Setting up port forwarding..." echo " - MCP Server: localhost:8000 -> $BENCHMARK_SERVICE.team1:8000" -echo " - A2A Agent: localhost:8080 -> $AGENT_SERVICE.team1:8080" +echo " - A2A Agent: localhost:8081 -> $AGENT_SERVICE.team1:8080" echo "" # Kill any existing port-forwards on these ports @@ -50,7 +50,7 @@ kubectl port-forward -n team1 svc/$BENCHMARK_SERVICE 8000:8000 & PF_MCP_PID=$! echo "Starting port-forward for A2A agent..." -kubectl port-forward -n team1 svc/$AGENT_SERVICE 8080:8080 & +kubectl port-forward -n team1 svc/$AGENT_SERVICE 8081:8080 & PF_AGENT_PID=$! # Wait for port forwards to be ready @@ -96,7 +96,7 @@ else fi echo -n " A2A Agent: " -if curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/.well-known/agent-card.json 2>/dev/null | grep -q "200\|404"; then +if curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/.well-known/agent-card.json 2>/dev/null | grep -q "200\|404"; then echo "✓ Reachable" else echo "⚠ May not be reachable - this might be OK" From 419f70d8788feae80cc1fe069229e5a35a71f37b Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 13:03:50 +0200 Subject: [PATCH 12/17] fix: correct tool naming and make configure script executable Changes: - Made configure-agent-environment.sh executable (chmod +x) - Fixed tool name in deploy-agent.sh: removed duplicate '-mcp' suffix from 'exgentic-mcp-${BENCHMARK_NAME}-mcp' to 'exgentic-mcp-${BENCHMARK_NAME}' - Fixed tool name in deploy-benchmark.sh: removed duplicate '-mcp' suffix from 'exgentic-mcp-${BENCHMARK_NAME}-mcp' to 'exgentic-mcp-${BENCHMARK_NAME}' This ensures consistent tool naming across deployment scripts and makes the configuration script directly executable. Signed-off-by: Yoav Katz --- exgentic_a2a_runner/configure-agent-environment.sh | 0 exgentic_a2a_runner/deploy-agent.sh | 2 +- exgentic_a2a_runner/deploy-benchmark.sh | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 exgentic_a2a_runner/configure-agent-environment.sh diff --git a/exgentic_a2a_runner/configure-agent-environment.sh b/exgentic_a2a_runner/configure-agent-environment.sh old mode 100644 new mode 100755 diff --git a/exgentic_a2a_runner/deploy-agent.sh b/exgentic_a2a_runner/deploy-agent.sh index b7c016d..ca31395 100755 --- a/exgentic_a2a_runner/deploy-agent.sh +++ b/exgentic_a2a_runner/deploy-agent.sh @@ -17,7 +17,7 @@ if [ -z "$BENCHMARK_NAME" ]; then fi AGENT_NAME="generic-agent-internal-${BENCHMARK_NAME}" -TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}-mcp" +TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}" NAMESPACE="team1" KAGENTI_API="http://localhost:8001" KAGENTI_PORT=8001 diff --git a/exgentic_a2a_runner/deploy-benchmark.sh b/exgentic_a2a_runner/deploy-benchmark.sh index 9393e8e..4a1fb3f 100755 --- a/exgentic_a2a_runner/deploy-benchmark.sh +++ b/exgentic_a2a_runner/deploy-benchmark.sh @@ -17,7 +17,7 @@ if [ -z "$BENCHMARK_NAME" ]; then fi IMAGE_NAME="localhost/exgentic-mcp-${BENCHMARK_NAME}:latest" -TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}-mcp" +TOOL_NAME="exgentic-mcp-${BENCHMARK_NAME}" NAMESPACE="team1" KAGENTI_API="http://localhost:8001" # Using 8001 to avoid conflict with MCP server on 8000 KAGENTI_PORT=8001 From 23f5aac2bc74cdd07859c1d6df4705c887de65ad Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 13:12:30 +0200 Subject: [PATCH 13/17] Updated documentation with new scripts Signed-off-by: Yoav Katz --- exgentic_a2a_runner/QUICKSTART.md | 144 ++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 26 deletions(-) diff --git a/exgentic_a2a_runner/QUICKSTART.md b/exgentic_a2a_runner/QUICKSTART.md index 5ba306e..d483a9a 100644 --- a/exgentic_a2a_runner/QUICKSTART.md +++ b/exgentic_a2a_runner/QUICKSTART.md @@ -2,59 +2,108 @@ ## Prerequisites -✅ kubectl configured with `kind-kagenti` context -✅ Services running in team1 namespace: -- `exgentic-mcp-tau2-mcp` on port 8000 -- `generic-agent2` on port 8080 +✅ kubectl configured with `kind-kagenti` context +✅ Kagenti cluster running with: +- Kagenti backend in `kagenti-system` namespace +- Keycloak in `keycloak` namespace +- `team1` namespace for deployments -## Quick Run +## Option 1: Deploy Your Own Benchmark and Agent -The easiest way to run the harness is using the provided script: +If you want to deploy a fresh benchmark and agent: + +### Step 1: Deploy the Benchmark (MCP Server) + +Deploy an Exgentic benchmark (e.g., GSM8K): ```bash cd exgentic_a2a_runner -./run-with-port-forward.sh +./deploy-benchmark.sh gsm8k admin admin ``` -This script will: -1. Set up port forwarding to both services -2. Test connectivity -3. Run the harness with verbose logging -4. Clean up port forwards on exit +This will: +- Check for the benchmark image locally +- Sync the image to the kind cluster if needed +- Deploy the MCP server via Kagenti API +- Wait for the deployment to be ready + +**Note:** You need to build the benchmark image first. Use +agent-examples/mcp/exgentic_benchmarks/build.sh to build the image locally. -## Manual Run +### Step 2: Deploy the Agent -If you prefer to set up port forwarding manually: +Deploy a generic A2A agent that will use the benchmark: -### Terminal 1: Port Forward MCP Server ```bash -kubectl port-forward -n team1 svc/exgentic-mcp-tau2-mcp 8000:8000 +./deploy-agent.sh gsm8k admin admin ``` -### Terminal 2: Port Forward A2A Agent +This will: +- Deploy the generic agent via Kagenti API +- Configure it to connect to the MCP server +- Build the agent image from source +- Wait for the deployment to be ready + +### Step 3: Update Configuration + +Update your `.env` file to use the deployed services: + ```bash -kubectl port-forward -n team1 svc/generic-agent2 8080:8080 +# Update these values in .env +AGENT_SERVICE=generic-agent-internal-gsm8k +BENCHMARK_SERVICE=exgentic-mcp-gsm8k-mcp ``` -### Terminal 3: Run Harness +### Step 4: Run the Harness + +Now run the harness with the deployed services: + +```bash +./run-with-port-forward.sh +``` + +## Option 2: Use Existing Services + +If services are already deployed in the team1 namespace, you can use them directly. + +### Quick Run + +The easiest way to run the harness is using the provided script: + ```bash cd exgentic_a2a_runner -source .venv/bin/activate -uv run exgentic-a2a-runner --verbose +./run-with-port-forward.sh ``` +This script will: +1. Set up port forwarding to both services +2. Test connectivity +3. Run the harness with verbose logging +4. Clean up port forwards on exit + + ## Configuration -The `.env` file is already configured for the Kagenti cluster: +The `.env` file should be configured for your deployed services: ```bash -EXGENTIC_MCP_SERVER_URL=http://localhost:8000 -A2A_BASE_URL=http://localhost:8080 -MAX_TASKS=3 # Start with 3 sessions for testing +# Service names in Kubernetes +AGENT_SERVICE=generic-agent-internal-gsm8k +BENCHMARK_SERVICE=exgentic-mcp-gsm8k-mcp + +# Local endpoints (via port-forward) +EXGENTIC_MCP_SERVER_URL=http://localhost:8000/mcp +A2A_BASE_URL=http://localhost:8081 + +# Test configuration +MAX_TASKS=10 # Number of tasks to run +MAX_PARALLEL_SESSIONS=10 # Parallel execution LOG_PROMPT=1 # Enable prompt logging LOG_RESPONSE=1 # Enable response logging ``` +**Important:** Note that the A2A agent now uses port 8081 (not 8080) to avoid conflicts with the kagenti-ui service. + ## What to Expect The harness will: @@ -130,13 +179,56 @@ If sessions complete but evaluation fails: - Review agent logs to see what tools it's calling - Verify the agent has access to the MCP server for tool execution +## Deployment Scripts Reference + +### deploy-benchmark.sh + +Deploys an Exgentic MCP benchmark server: + +```bash +./deploy-benchmark.sh [keycloak-username] [keycloak-password] +``` + +**Arguments:** +- `benchmark-name`: Name of the benchmark (e.g., gsm8k, tau2) +- `keycloak-username`: Keycloak admin username (default: admin) +- `keycloak-password`: Keycloak admin password (default: admin) + +**What it does:** +1. Checks for local benchmark image +2. Syncs image to kind cluster if needed +3. Authenticates with Keycloak +4. Deploys MCP server via Kagenti API +5. Waits for deployment to be ready + +### deploy-agent.sh + +Deploys a generic A2A agent: + +```bash +./deploy-agent.sh [keycloak-username] [keycloak-password] +``` + +**Arguments:** +- `benchmark-name`: Name of the benchmark the agent will use +- `keycloak-username`: Keycloak admin username (default: admin) +- `keycloak-password`: Keycloak admin password (default: admin) + +**What it does:** +1. Authenticates with Keycloak +2. Fetches agent environment configuration +3. Configures agent to connect to the MCP server +4. Deploys agent via Kagenti API (builds from source) +5. Waits for build and deployment to complete + ## Next Steps After successful test run: 1. Increase `MAX_TASKS` in `.env` for longer runs 2. Enable OTLP exporter for telemetry collection -3. Run with different benchmarks by changing the MCP server +3. Deploy different benchmarks and test with various agents 4. Analyze results and agent performance +5. Access kagenti-ui at http://kagenti-ui.localtest.me:8080/ to monitor deployments ## Support From 32943c1cc05aa89b8a4d1aedd510470127f0383c Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 13:26:52 +0200 Subject: [PATCH 14/17] docs: update QUICKSTART with deployment instructions and fix Keycloak auth Changes: - Updated QUICKSTART.md with comprehensive deployment instructions - Added Option 1: Deploy Your Own Benchmark and Agent - Added Option 2: Use Existing Services - Documented deploy-benchmark.sh and deploy-agent.sh usage - Updated configuration section with new port (8081) for A2A agent - Added reference documentation for deployment scripts - Fixed Keycloak authentication error in deployment scripts - Added automatic enabling of Direct Access Grants for kagenti client - Both deploy-benchmark.sh and deploy-agent.sh now configure Keycloak - Added better error messages for authentication failures - Renumbered steps after adding Keycloak configuration step This resolves the 'unauthorized_client' error when running deployment scripts and provides clear documentation for deploying benchmarks and agents to the Kagenti cluster. Signed-off-by: Yoav Katz --- exgentic_a2a_runner/deploy-agent.sh | 70 +++++++++++++++++++------ exgentic_a2a_runner/deploy-benchmark.sh | 61 ++++++++++++++++----- 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/exgentic_a2a_runner/deploy-agent.sh b/exgentic_a2a_runner/deploy-agent.sh index ca31395..d997089 100755 --- a/exgentic_a2a_runner/deploy-agent.sh +++ b/exgentic_a2a_runner/deploy-agent.sh @@ -42,8 +42,42 @@ fi echo "" -# Step 2: Get Keycloak authentication token -echo "Step 2: Getting Keycloak authentication token..." +# Step 2: Enable Direct Access Grants for kagenti client if needed +echo "Step 2: Checking Keycloak client configuration..." + +# Get admin token first +ADMIN_TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$KEYCLOAK_USERNAME" \ + -d "password=$KEYCLOAK_PASSWORD" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" 2>/dev/null || echo "TOKEN_ERROR") + +if [ "$ADMIN_TOKEN_RESPONSE" != "TOKEN_ERROR" ]; then + ADMIN_TOKEN=$(echo "$ADMIN_TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed 's/"access_token":"\([^"]*\)"/\1/') + + if [ -n "$ADMIN_TOKEN" ]; then + # Get kagenti client configuration + CLIENT_CONFIG=$(curl -s "$KEYCLOAK_API/admin/realms/kagenti/clients?clientId=kagenti" \ + -H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null) + + CLIENT_ID=$(echo "$CLIENT_CONFIG" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') + + if [ -n "$CLIENT_ID" ]; then + # Enable direct access grants + curl -s -X PUT "$KEYCLOAK_API/admin/realms/kagenti/clients/$CLIENT_ID" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"directAccessGrantsEnabled": true}' >/dev/null 2>&1 + echo "✓ Direct access grants enabled for kagenti client" + fi + fi +fi + +echo "" + +# Step 3: Get Keycloak authentication token +echo "Step 3: Getting Keycloak authentication token..." TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/kagenti/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=$KEYCLOAK_USERNAME" \ @@ -60,6 +94,10 @@ ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed ' if [ -z "$ACCESS_TOKEN" ]; then echo "Error: Failed to extract access token" + echo "Response: $TOKEN_RESPONSE" + echo "" + echo "If you see 'unauthorized_client' error, the kagenti client may need Direct Access Grants enabled." + echo "You can enable it manually in Keycloak admin console or run this script again." exit 1 fi @@ -67,8 +105,8 @@ echo "✓ Successfully obtained authentication token" echo "" -# Step 3: Set up port-forward to Kagenti backend -echo "Step 3: Setting up port-forward to Kagenti backend..." +# Step 4: Set up port-forward to Kagenti backend +echo "Step 4: Setting up port-forward to Kagenti backend..." if lsof -Pi :$KAGENTI_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then echo "✓ Port $KAGENTI_PORT is already in use (assuming Kagenti backend is accessible)" else @@ -80,8 +118,8 @@ fi echo "" -# Step 4: Delete existing agent if it exists -echo "Step 4: Deleting existing agent via Kagenti API if it exists..." +# Step 5: Delete existing agent if it exists +echo "Step 5: Deleting existing agent via Kagenti API if it exists..." DELETE_RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/kagenti_delete_agent_response.txt -X DELETE "$KAGENTI_API/api/v1/agents/$NAMESPACE/$AGENT_NAME" \ -H "Authorization: Bearer $ACCESS_TOKEN") @@ -95,8 +133,8 @@ sleep 3 echo "" -# Step 5: Fetch and parse environment variables -echo "Step 5: Fetching environment variables..." +# Step 6: Fetch and parse environment variables +echo "Step 6: Fetching environment variables..." ENV_CONTENT=$(curl -s https://raw.githubusercontent.com/kagenti/agent-examples/refs/heads/main/a2a/generic_agent/.env.openai) # Parse env vars using the Kagenti API @@ -111,8 +149,8 @@ echo "✓ Environment variables parsed" echo "" -# Step 6: Deploy agent via Kagenti API -echo "Step 6: Deploying agent via Kagenti API..." +# Step 7: Deploy agent via Kagenti API +echo "Step 7: Deploying agent via Kagenti API..." # Add MCP_URLS to environment variables MCP_URL="http://${TOOL_NAME}-mcp:8000/mcp" @@ -172,8 +210,8 @@ fi echo "" -# Step 7: Wait for build to complete -echo "Step 7: Waiting for build to complete..." +# Step 8: Wait for build to complete +echo "Step 8: Waiting for build to complete..." BUILD_RUN_NAME=$(echo "$RESPONSE" | jq -r '.message' | grep -o "BuildRun: '[^']*'" | sed "s/BuildRun: '\([^']*\)'/\1/") if [ -z "$BUILD_RUN_NAME" ]; then @@ -209,8 +247,8 @@ fi echo "" -# Step 8: Wait for agent deployment to be created and ready -echo "Step 8: Waiting for agent deployment to be created..." +# Step 9: Wait for agent deployment to be created and ready +echo "Step 9: Waiting for agent deployment to be created..." # Wait for deployment to be created (up to 2 minutes) for i in {1..24}; do @@ -239,8 +277,8 @@ fi echo "✓ Agent deployment is ready" echo "" -# Step 9: Test agent card access -echo "Step 9: Testing agent card access..." +# Step 10: Test agent card access +echo "Step 10: Testing agent card access..." # Set up port-forward to agent AGENT_PORT=8084 diff --git a/exgentic_a2a_runner/deploy-benchmark.sh b/exgentic_a2a_runner/deploy-benchmark.sh index 4a1fb3f..cde4c87 100755 --- a/exgentic_a2a_runner/deploy-benchmark.sh +++ b/exgentic_a2a_runner/deploy-benchmark.sh @@ -128,8 +128,42 @@ fi echo "" -# Step 4: Get Keycloak authentication token... -echo "Step 4: Getting Keycloak authentication token..." +# Step 4: Enable Direct Access Grants for kagenti client if needed +echo "Step 4: Checking Keycloak client configuration..." + +# Get admin token first +ADMIN_TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$KEYCLOAK_USERNAME" \ + -d "password=$KEYCLOAK_PASSWORD" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" 2>/dev/null || echo "TOKEN_ERROR") + +if [ "$ADMIN_TOKEN_RESPONSE" != "TOKEN_ERROR" ]; then + ADMIN_TOKEN=$(echo "$ADMIN_TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed 's/"access_token":"\([^"]*\)"/\1/') + + if [ -n "$ADMIN_TOKEN" ]; then + # Get kagenti client configuration + CLIENT_CONFIG=$(curl -s "$KEYCLOAK_API/admin/realms/kagenti/clients?clientId=kagenti" \ + -H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null) + + CLIENT_ID=$(echo "$CLIENT_CONFIG" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') + + if [ -n "$CLIENT_ID" ]; then + # Enable direct access grants + curl -s -X PUT "$KEYCLOAK_API/admin/realms/kagenti/clients/$CLIENT_ID" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"directAccessGrantsEnabled": true}' >/dev/null 2>&1 + echo "✓ Direct access grants enabled for kagenti client" + fi + fi +fi + +echo "" + +# Step 5: Get Keycloak authentication token... +echo "Step 5: Getting Keycloak authentication token..." # Get token from Keycloak using kagenti client (with direct access grants enabled) TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_API/realms/kagenti/protocol/openid-connect/token" \ @@ -150,6 +184,9 @@ ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | sed ' if [ -z "$ACCESS_TOKEN" ]; then echo "Error: Failed to extract access token from Keycloak response" echo "Response: $TOKEN_RESPONSE" + echo "" + echo "If you see 'unauthorized_client' error, the kagenti client may need Direct Access Grants enabled." + echo "You can enable it manually in Keycloak admin console or run this script again." exit 1 fi @@ -157,8 +194,8 @@ echo "✓ Successfully obtained authentication token" echo "" -# Step 5: Set up port-forward to Kagenti backend -echo "Step 5: Setting up port-forward to Kagenti backend..." +# Step 6: Set up port-forward to Kagenti backend +echo "Step 6: Setting up port-forward to Kagenti backend..." # Check if port-forward is already running if lsof -Pi :$KAGENTI_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then @@ -186,8 +223,8 @@ fi echo "" -# Step 6: Delete existing tool via Kagenti API if it exists -echo "Step 6: Deleting existing tool via Kagenti API if it exists..." +# Step 7: Delete existing tool via Kagenti API if it exists +echo "Step 7: Deleting existing tool via Kagenti API if it exists..." DELETE_RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/kagenti_delete_response.txt -X DELETE "$KAGENTI_API/api/v1/tools/$NAMESPACE/$TOOL_NAME" \ -H "Authorization: Bearer $ACCESS_TOKEN") @@ -203,8 +240,8 @@ sleep 3 echo "" -# Step 7: Deploy tool using Kagenti API -echo "Step 7: Deploying tool via Kagenti API..." +# Step 8: Deploy tool using Kagenti API +echo "Step 8: Deploying tool via Kagenti API..." # Create tool deployment JSON following Kagenti API format TOOL_JSON=$(cat </dev/null || echo "Warning: Could not patch imagePullPolicy" echo "✓ ImagePullPolicy patched" echo "" -# Step 9: Wait for tool to be ready -echo "Step 9: Waiting for tool to be ready..." +# Step 10: Wait for tool to be ready +echo "Step 10: Waiting for tool to be ready..." MAX_WAIT=120 WAIT_INTERVAL=5 From af33636f2921cb0d7ed4537de984dbf3537558f2 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 14:30:39 +0200 Subject: [PATCH 15/17] feat: enhance deployment and configuration scripts Changes: 1. Renamed configure-agent-environment.sh to configure-agent-and-benchmark-environment.sh - Use 'kubectl set env' instead of JSON patch for cleaner updates - Extended script to configure both agent and benchmark deployments - Added clear separation between agent and benchmark configuration sections - Improved output formatting with dedicated sections for each component - Added deployment-specific configuration summaries - Agent gets: LLM_API_BASE, OPENAI_API_BASE, LLM_MODEL - Benchmark gets: OPENAI_API_BASE, EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL 2. Enhanced deploy-benchmark.sh - Added fetching and parsing of benchmark-specific environment variables - Fetches .env. from agent-examples repository - Parses environment variables using Kagenti API - Includes env vars in tool deployment configuration - Added graceful handling when env file is not found - Renumbered steps after adding env var fetching step These improvements ensure: - Consistent LLM configuration across agent and benchmark - Better visibility into what's being configured - Benchmark-specific settings are properly applied from repository - Clearer output for troubleshooting - Proper separation of concerns between agent and benchmark configuration Signed-off-by: Yoav Katz --- ...nfigure-agent-and-benchmark-environment.sh | 128 ++++++++++++++++++ .../configure-agent-environment.sh | 103 -------------- exgentic_a2a_runner/deploy-benchmark.sh | 39 +++++- 3 files changed, 161 insertions(+), 109 deletions(-) create mode 100755 exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh delete mode 100755 exgentic_a2a_runner/configure-agent-environment.sh diff --git a/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh b/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh new file mode 100755 index 0000000..b006dca --- /dev/null +++ b/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Configure agent and benchmark environment settings +# Usage: ./configure-agent-environment.sh +# Example: ./configure-agent-environment.sh gsm8k +# This script updates the Kubernetes secret and environment variables for both agent and benchmark + +set -e + +BENCHMARK_NAME="$1" + +if [ -z "$BENCHMARK_NAME" ]; then + echo "Error: Benchmark name is required" + echo "Usage: $0 " + echo "Example: $0 gsm8k" + exit 1 +fi + +NAMESPACE="team1" +AGENT_NAME="generic-agent-internal-${BENCHMARK_NAME}" +BENCHMARK_DEPLOYMENT="exgentic-mcp-${BENCHMARK_NAME}" + +echo "==========================================" +echo "Configuring Environment" +echo "==========================================" +echo "Agent: $AGENT_NAME" +echo "Benchmark: $BENCHMARK_DEPLOYMENT" +echo "" + +# Step 1: Update the openai-secret with current OPENAI_API_KEY +echo "Step 1: Updating openai-secret with OPENAI_API_KEY..." + +if [ -z "$OPENAI_API_KEY" ]; then + echo "Error: OPENAI_API_KEY environment variable is not set" + exit 1 +fi + +# Encode the API key in base64 +ENCODED_KEY=$(echo -n "$OPENAI_API_KEY" | base64) + +# Patch the secret +kubectl patch secret openai-secret -n $NAMESPACE --type='json' -p="[ + { + \"op\": \"replace\", + \"path\": \"/data/apikey\", + \"value\": \"$ENCODED_KEY\" + } +]" + +echo "✓ Secret updated" +echo "" + +echo "==========================================" +echo "AGENT CONFIGURATION" +echo "==========================================" +echo "" + +# Step 2: Update agent deployment with Azure OpenAI settings +echo "Step 2: Updating agent deployment with Azure OpenAI settings..." + +if [ -z "$OPENAI_API_BASE" ]; then + echo "Error: OPENAI_API_BASE environment variable is not set" + exit 1 +fi + +# Use kubectl set env to update environment variables +kubectl set env deployment/$AGENT_NAME -n $NAMESPACE \ + LLM_API_BASE="$OPENAI_API_BASE" \ + OPENAI_API_BASE="$OPENAI_API_BASE" \ + LLM_MODEL="Azure/gpt-4o" + +echo "✓ Agent environment variables updated" +echo "" + +# Step 3: Wait for agent rollout +echo "Step 3: Waiting for agent deployment rollout..." +kubectl rollout status deployment/$AGENT_NAME -n $NAMESPACE --timeout=120s + +echo "✓ Agent rollout complete" +echo "" + +echo "Agent configuration applied:" +echo " Deployment: $AGENT_NAME" +echo " LLM_API_BASE: $OPENAI_API_BASE" +echo " OPENAI_API_BASE: $OPENAI_API_BASE" +echo " LLM_MODEL: Azure/gpt-4o" +echo " OPENAI_API_KEY: (updated secret from env var)" +echo "" + +echo "==========================================" +echo "BENCHMARK CONFIGURATION" +echo "==========================================" +echo "" + +# Step 4: Update benchmark deployment with Azure OpenAI settings +echo "Step 4: Updating benchmark deployment with Azure OpenAI settings..." + +# Check if benchmark deployment exists +if kubectl get deployment $BENCHMARK_DEPLOYMENT -n $NAMESPACE >/dev/null 2>&1; then + kubectl set env deployment/$BENCHMARK_DEPLOYMENT -n $NAMESPACE \ + OPENAI_API_BASE="$OPENAI_API_BASE" \ + EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL=openai/Azure/gpt-4o + + echo "✓ Benchmark environment variables updated" + echo "" + + # Step 5: Wait for benchmark rollout + echo "Step 5: Waiting for benchmark deployment rollout..." + kubectl rollout status deployment/$BENCHMARK_DEPLOYMENT -n $NAMESPACE --timeout=120s + echo "✓ Benchmark rollout complete" + echo "" + + echo "Benchmark configuration applied:" + echo " Deployment: $BENCHMARK_DEPLOYMENT" + echo " OPENAI_API_BASE: $OPENAI_API_BASE" + echo " EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL: openai/Azure/gpt-4o" + echo " OPENAI_API_KEY: (updated secret from env var)" +else + echo "⚠ Benchmark deployment not found, skipping" +fi + +echo "" + +echo "==========================================" +echo "Configuration Complete!" +echo "==========================================" +echo "" + +# Made with Bob diff --git a/exgentic_a2a_runner/configure-agent-environment.sh b/exgentic_a2a_runner/configure-agent-environment.sh deleted file mode 100755 index e0e92eb..0000000 --- a/exgentic_a2a_runner/configure-agent-environment.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# Configure agent environment settings -# Usage: ./configure-agent-environment.sh -# Example: ./configure-agent-environment.sh gsm8k -# This script updates the Kubernetes secret and patches the agent deployment - -set -e - -BENCHMARK_NAME="$1" - -if [ -z "$BENCHMARK_NAME" ]; then - echo "Error: Benchmark name is required" - echo "Usage: $0 " - echo "Example: $0 gsm8k" - exit 1 -fi - -NAMESPACE="team1" -AGENT_NAME="generic-agent-internal-${BENCHMARK_NAME}" - -echo "==========================================" -echo "Configuring Agent Environment: $AGENT_NAME" -echo "==========================================" -echo "" - -# Step 1: Update the openai-secret with current OPENAI_API_KEY -echo "Step 1: Updating openai-secret with OPENAI_API_KEY..." - -if [ -z "$OPENAI_API_KEY" ]; then - echo "Error: OPENAI_API_KEY environment variable is not set" - exit 1 -fi - -# Encode the API key in base64 -ENCODED_KEY=$(echo -n "$OPENAI_API_KEY" | base64) - -# Patch the secret -kubectl patch secret openai-secret -n $NAMESPACE --type='json' -p="[ - { - \"op\": \"replace\", - \"path\": \"/data/apikey\", - \"value\": \"$ENCODED_KEY\" - } -]" - -echo "✓ Secret updated" -echo "" - -# Step 2: Patch agent deployment with Azure OpenAI settings -echo "Step 2: Patching agent deployment with Azure OpenAI settings..." - -if [ -z "$OPENAI_API_BASE" ]; then - echo "Error: OPENAI_API_BASE environment variable is not set" - exit 1 -fi - -# Get current env vars -CURRENT_ENV=$(kubectl get deployment $AGENT_NAME -n $NAMESPACE -o json | jq '.spec.template.spec.containers[0].env') - -# Find indices of the env vars we need to update -LLM_API_BASE_INDEX=$(echo "$CURRENT_ENV" | jq 'map(.name == "LLM_API_BASE") | index(true)') -LLM_MODEL_INDEX=$(echo "$CURRENT_ENV" | jq 'map(.name == "LLM_MODEL") | index(true)') - -if [ "$LLM_API_BASE_INDEX" = "null" ] || [ "$LLM_MODEL_INDEX" = "null" ]; then - echo "Error: Could not find LLM_API_BASE or LLM_MODEL in deployment" - exit 1 -fi - -# Patch only the specific env vars -kubectl patch deployment $AGENT_NAME -n $NAMESPACE --type='json' -p="[ - { - \"op\": \"replace\", - \"path\": \"/spec/template/spec/containers/0/env/$LLM_API_BASE_INDEX/value\", - \"value\": \"$OPENAI_API_BASE\" - }, - { - \"op\": \"replace\", - \"path\": \"/spec/template/spec/containers/0/env/$LLM_MODEL_INDEX/value\", - \"value\": \"Azure/gpt-4o\" - } -]" - -echo "✓ Deployment patched" -echo "" - -# Step 3: Wait for rollout -echo "Step 3: Waiting for deployment rollout..." -kubectl rollout status deployment/$AGENT_NAME -n $NAMESPACE --timeout=120s - -echo "✓ Rollout complete" -echo "" - -echo "==========================================" -echo "Configuration Complete!" -echo "==========================================" -echo "" -echo "Settings applied:" -echo " LLM_API_BASE: $OPENAI_API_BASE" -echo " LLM_MODEL: Azure/gpt-4o" -echo " OPENAI_API_KEY: (from secret)" -echo "" - -# Made with Bob diff --git a/exgentic_a2a_runner/deploy-benchmark.sh b/exgentic_a2a_runner/deploy-benchmark.sh index cde4c87..d97e7dd 100755 --- a/exgentic_a2a_runner/deploy-benchmark.sh +++ b/exgentic_a2a_runner/deploy-benchmark.sh @@ -240,8 +240,34 @@ sleep 3 echo "" -# Step 8: Deploy tool using Kagenti API -echo "Step 8: Deploying tool via Kagenti API..." +# Step 8: Fetch and parse benchmark environment variables +echo "Step 8: Fetching benchmark environment variables..." +ENV_CONTENT=$(curl -s "https://raw.githubusercontent.com/yoavkatz/agent-examples/refs/heads/feature/exgentic-mcp-server/mcp/exgentic_benchmarks/.env.${BENCHMARK_NAME}") + +if [ -z "$ENV_CONTENT" ] || echo "$ENV_CONTENT" | grep -q "404: Not Found"; then + echo "Warning: Could not fetch .env.${BENCHMARK_NAME} file, deploying without custom env vars" + ENV_VARS="[]" +else + # Parse env vars using the Kagenti API + ENV_PARSE_RESPONSE=$(curl -s -X POST "$KAGENTI_API/api/v1/agents/parse-env" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -d "{\"content\": $(echo "$ENV_CONTENT" | jq -Rs .)}") + + ENV_VARS=$(echo "$ENV_PARSE_RESPONSE" | jq '.envVars') + + if [ "$ENV_VARS" = "null" ] || [ -z "$ENV_VARS" ]; then + echo "Warning: Could not parse environment variables, deploying without custom env vars" + ENV_VARS="[]" + else + echo "✓ Environment variables parsed" + fi +fi + +echo "" + +# Step 9: Deploy tool using Kagenti API +echo "Step 9: Deploying tool via Kagenti API..." # Create tool deployment JSON following Kagenti API format TOOL_JSON=$(cat </dev/null || echo "Warning: Could not patch imagePullPolicy" echo "✓ ImagePullPolicy patched" echo "" -# Step 10: Wait for tool to be ready -echo "Step 10: Waiting for tool to be ready..." +# Step 11: Wait for tool to be ready +echo "Step 11: Waiting for tool to be ready..." MAX_WAIT=120 WAIT_INTERVAL=5 From 65c44194556fc0fb2d94bebdd690049855497c85 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Sun, 22 Mar 2026 16:32:36 +0200 Subject: [PATCH 16/17] fix: improve port-forward cleanup in run script Changed port-forward cleanup to kill processes by port number instead of service name. This ensures all existing port-forwards on ports 8000 and 8081 are cleaned up regardless of which benchmark or agent service they were forwarding to. Uses lsof to find processes using the ports and kills them, making the script more robust when switching between different benchmarks/agents. Signed-off-by: Yoav Katz --- exgentic_a2a_runner/run-with-port-forward.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh index e047fcb..b1ea642 100755 --- a/exgentic_a2a_runner/run-with-port-forward.sh +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -39,9 +39,11 @@ echo " - A2A Agent: localhost:8081 -> $AGENT_SERVICE.team1:8080" echo "" # Kill any existing port-forwards on these ports -echo "Cleaning up existing port-forwards..." -pkill -f "port-forward.*$BENCHMARK_SERVICE" 2>/dev/null || true -pkill -f "port-forward.*$AGENT_SERVICE" 2>/dev/null || true +echo "Cleaning up existing port-forwards on ports 8000 and 8081..." +# Kill any process using port 8000 +lsof -ti:8000 | xargs kill -9 2>/dev/null || true +# Kill any process using port 8081 +lsof -ti:8081 | xargs kill -9 2>/dev/null || true sleep 2 # Start port forwarding in background From d080211d9e17ee1a1ffecd963e210f56dba0d0a5 Mon Sep 17 00:00:00 2001 From: Yoav Katz Date: Mon, 23 Mar 2026 18:44:02 +0200 Subject: [PATCH 17/17] feat: improve benchmark deployment and session management - Add resource limits (2Gi memory) to benchmark pod deployments - Rename close_session to delete_session throughout the stack - Add validation for delete_session response (supports both 'success' and 'status' fields) - Conditionally set EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL only for tau benchmarks - Create evaluate_benchmark.sh script that accepts benchmark name as parameter - Set AGENT_SERVICE and BENCHMARK_SERVICE dynamically based on benchmark name Signed-off-by: Yoav Katz --- ...nfigure-agent-and-benchmark-environment.sh | 18 +- exgentic_a2a_runner/deploy-benchmark.sh | 10 + exgentic_a2a_runner/evaluate_benchmark.sh | 187 ++++++++++++++++++ .../exgentic_a2a_runner/exgentic_adapter.py | 14 +- .../exgentic_a2a_runner/mcp_client.py | 48 +++-- .../exgentic_a2a_runner/runner.py | 16 +- exgentic_a2a_runner/run-with-port-forward.sh | 32 ++- 7 files changed, 291 insertions(+), 34 deletions(-) create mode 100755 exgentic_a2a_runner/evaluate_benchmark.sh diff --git a/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh b/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh index b006dca..8ade9c0 100755 --- a/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh +++ b/exgentic_a2a_runner/configure-agent-and-benchmark-environment.sh @@ -96,11 +96,19 @@ echo "Step 4: Updating benchmark deployment with Azure OpenAI settings..." # Check if benchmark deployment exists if kubectl get deployment $BENCHMARK_DEPLOYMENT -n $NAMESPACE >/dev/null 2>&1; then + # Set OPENAI_API_BASE for all benchmarks kubectl set env deployment/$BENCHMARK_DEPLOYMENT -n $NAMESPACE \ - OPENAI_API_BASE="$OPENAI_API_BASE" \ - EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL=openai/Azure/gpt-4o + OPENAI_API_BASE="$OPENAI_API_BASE" + + # Only set EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL for tau benchmarks + if [[ "$BENCHMARK_NAME" == tau* ]]; then + kubectl set env deployment/$BENCHMARK_DEPLOYMENT -n $NAMESPACE \ + EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL=openai/Azure/gpt-4o + echo "✓ Benchmark environment variables updated (including user simulator model for tau benchmark)" + else + echo "✓ Benchmark environment variables updated" + fi - echo "✓ Benchmark environment variables updated" echo "" # Step 5: Wait for benchmark rollout @@ -112,7 +120,9 @@ if kubectl get deployment $BENCHMARK_DEPLOYMENT -n $NAMESPACE >/dev/null 2>&1; t echo "Benchmark configuration applied:" echo " Deployment: $BENCHMARK_DEPLOYMENT" echo " OPENAI_API_BASE: $OPENAI_API_BASE" - echo " EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL: openai/Azure/gpt-4o" + if [[ "$BENCHMARK_NAME" == tau* ]]; then + echo " EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL: openai/Azure/gpt-4o" + fi echo " OPENAI_API_KEY: (updated secret from env var)" else echo "⚠ Benchmark deployment not found, skipping" diff --git a/exgentic_a2a_runner/deploy-benchmark.sh b/exgentic_a2a_runner/deploy-benchmark.sh index d97e7dd..640a0ed 100755 --- a/exgentic_a2a_runner/deploy-benchmark.sh +++ b/exgentic_a2a_runner/deploy-benchmark.sh @@ -280,6 +280,16 @@ TOOL_JSON=$(cat < +# Example: ./evaluate_benchmark.sh gsm8k + +set -e + +BENCHMARK_NAME="$1" + +if [ -z "$BENCHMARK_NAME" ]; then + echo "Error: Benchmark name is required" + echo "Usage: $0 " + echo "Example: $0 gsm8k" + exit 1 +fi + +# Set service names based on benchmark name +export AGENT_SERVICE="generic-agent-internal-${BENCHMARK_NAME}" +export BENCHMARK_SERVICE="exgentic-mcp-${BENCHMARK_NAME}" + +echo "==========================================" +echo "Exgentic A2A Runner - Benchmark Evaluation" +echo "==========================================" +echo "Benchmark: $BENCHMARK_NAME" +echo "Agent Service: $AGENT_SERVICE" +echo "Benchmark Service: $BENCHMARK_SERVICE" +echo "" + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "Error: kubectl is not installed or not in PATH" + exit 1 +fi + +# Check if we're connected to the right cluster +CURRENT_CONTEXT=$(kubectl config current-context) +echo "Current kubectl context: $CURRENT_CONTEXT" + +if [ "$CURRENT_CONTEXT" != "kind-kagenti" ]; then + echo "Warning: Not connected to kind-kagenti cluster" + read -p "Continue anyway? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Load environment variables if .env exists +if [ -f "$(dirname "$0")/.env" ]; then + source "$(dirname "$0")/.env" +fi + +echo "" +echo "Setting up port forwarding..." +echo " - MCP Server: localhost:8000 -> $BENCHMARK_SERVICE.team1:8000" +echo " - A2A Agent: localhost:8081 -> $AGENT_SERVICE.team1:8080" +echo "" + +# Kill any existing port-forwards on these ports +echo "Cleaning up existing port-forwards on ports 8000 and 8081..." +# Kill any process using port 8000 +lsof -ti:8000 | xargs kill -9 2>/dev/null || true +# Kill any process using port 8081 +lsof -ti:8081 | xargs kill -9 2>/dev/null || true +sleep 2 + +# Check if pods are ready before port-forwarding +echo "Checking if pods are ready..." + +# Extract deployment names (remove -mcp suffix from BENCHMARK_SERVICE if present) +BENCHMARK_DEPLOYMENT="${BENCHMARK_SERVICE%-mcp}" +AGENT_DEPLOYMENT="$AGENT_SERVICE" + +# Wait for MCP server pod to be ready +echo " Checking MCP server pod..." +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=$BENCHMARK_DEPLOYMENT -n team1 --timeout=60s +if [ $? -ne 0 ]; then + echo "Error: MCP server pod is not ready" + exit 1 +fi + +# Wait for agent pod to be ready +echo " Checking agent pod..." +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=$AGENT_DEPLOYMENT -n team1 --timeout=60s +if [ $? -ne 0 ]; then + echo "Error: Agent pod is not ready" + exit 1 +fi + +echo "✓ All pods are ready" +echo "" + +# Additional wait to ensure services are fully started +echo "Waiting for services to be fully started..." +sleep 10 + +# Start port forwarding in background +echo "Starting port-forward for MCP server..." +kubectl port-forward -n team1 svc/$BENCHMARK_SERVICE 8000:8000 & +PF_MCP_PID=$! + +echo "Starting port-forward for A2A agent..." +kubectl port-forward -n team1 svc/$AGENT_SERVICE 8081:8080 & +PF_AGENT_PID=$! + +# Wait for port forwards to be ready +echo "Waiting for port forwards to be ready..." +sleep 5 + +# Check if port forwards are working +if ! ps -p $PF_MCP_PID > /dev/null; then + echo "Error: MCP port-forward failed to start" + exit 1 +fi + +if ! ps -p $PF_AGENT_PID > /dev/null; then + echo "Error: Agent port-forward failed to start" + kill $PF_MCP_PID 2>/dev/null || true + exit 1 +fi + +echo "" +echo "✓ Port forwarding established" +echo " MCP Server PID: $PF_MCP_PID" +echo " A2A Agent PID: $PF_AGENT_PID" +echo "" + +# Function to cleanup on exit +#cleanup() { +# echo "" +# echo "Cleaning up port forwards..." +# kill $PF_MCP_PID 2>/dev/null || true +# kill $PF_AGENT_PID 2>/dev/null || true +# echo "Done." +#} +# +#trap cleanup EXIT INT TERM + +# Test connectivity +echo "Testing connectivity..." +echo -n " MCP Server: " +if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null | grep -q "200\|404"; then + echo "✓ Reachable" +else + echo "⚠ May not be reachable - this might be OK if no /health endpoint" +fi + +echo -n " A2A Agent: " +if curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/.well-known/agent-card.json 2>/dev/null | grep -q "200\|404"; then + echo "✓ Reachable" +else + echo "⚠ May not be reachable - this might be OK" +fi + +echo "" +echo "==========================================" +echo "Starting Exgentic A2A Runner" +echo "==========================================" +echo "" + +# Change to the script directory +cd "$(dirname "$0")" + +# Check if virtual environment exists +if [ ! -d ".venv" ]; then + echo "Virtual environment not found. Installing dependencies..." + uv sync --python 3.12 +fi + +# Activate virtual environment and run +source .venv/bin/activate + +# Load environment variables +if [ -f ".env" ]; then + echo "Loading environment variables from .env" + export $(cat .env | grep -v '^#' | xargs) + echo "" +fi + +# Run the harness +echo "Running: uv run exgentic-a2a-runner --verbose" +echo "" +uv run exgentic-a2a-runner --verbose + +# Cleanup will happen automatically via trap + +# Made with Bob \ No newline at end of file diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py index 7693572..dd12b78 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/exgentic_adapter.py @@ -112,26 +112,26 @@ def evaluate_session(self, session_id: str) -> bool: logger.error(f"Failed to evaluate session {session_id}: {e}") raise - def close_session(self, session_id: str) -> None: - """Close a benchmark session. + def delete_session(self, session_id: str) -> None: + """Delete a benchmark session. Args: session_id: Session identifier Raises: - RuntimeError: If adapter not initialized or close fails + RuntimeError: If adapter not initialized or deletion fails """ if not self._initialized: raise RuntimeError("Exgentic adapter not initialized. Call initialize() first.") - logger.info(f"Closing session: {session_id}") + logger.info(f"Deleting session: {session_id}") try: - self.mcp_client.close_session(session_id) - logger.info(f"Session {session_id} closed successfully") + self.mcp_client.delete_session(session_id) + logger.info(f"Session {session_id} deleted successfully") except Exception as e: - logger.error(f"Failed to close session {session_id}: {e}") + logger.error(f"Failed to delete session {session_id}: {e}") raise def get_task_ids(self) -> list[str]: diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py index 987b50f..9253a06 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/mcp_client.py @@ -225,37 +225,57 @@ async def _async_evaluate_session(self, session_id: str) -> Dict[str, Any]: else: raise RuntimeError(f"Unexpected content type: {type(content)}") - def close_session(self, session_id: str) -> None: - """Close a benchmark session. + def delete_session(self, session_id: str) -> None: + """Delete a benchmark session. Args: - session_id: Session ID to close + session_id: Session ID to delete Raises: - RuntimeError: If session closure fails + RuntimeError: If session deletion fails """ if not self._initialized: raise RuntimeError("MCP client not initialized") - logger.info(f"Closing session {session_id}") + logger.info(f"Deleting session {session_id}") try: - asyncio.run(self._async_close_session(session_id)) - logger.info(f"Session {session_id} closed") + asyncio.run(self._async_delete_session(session_id)) + logger.info(f"Session {session_id} deleted") except Exception as e: - logger.error(f"Failed to close session {session_id}: {e}") - raise RuntimeError(f"Session closure failed: {e}") + logger.error(f"Failed to delete session {session_id}: {e}") + raise RuntimeError(f"Session deletion failed: {e}") - async def _async_close_session(self, session_id: str) -> None: - """Async session closure.""" + async def _async_delete_session(self, session_id: str) -> None: + """Async session deletion.""" async with streamable_http_client(self.mcp_url) as (read, write, get_session_id): async with ClientSession(read, write) as session: await session.initialize() - # Call close_session tool - await session.call_tool( - "close_session", + # Call delete_session tool + result = await session.call_tool( + "delete_session", arguments={"session_id": session_id} ) + + # Check if the operation was successful + if not result.content: + raise RuntimeError("Empty response from delete_session") + + # Check if it's an error response + if result.isError: + content = result.content[0] + error_msg = content.text if hasattr(content, 'text') else str(content) + raise RuntimeError(f"Failed to delete session: {error_msg}") + + # Verify success response + content = result.content[0] + if hasattr(content, 'text'): + import json + response = json.loads(content.text) + # Check for either "success" field or "status" field + status = response.get("status", "") + if not (status == "success"): + raise RuntimeError(f"Session deletion failed: {response}") diff --git a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py index c38b0c1..4abfed5 100644 --- a/exgentic_a2a_runner/exgentic_a2a_runner/runner.py +++ b/exgentic_a2a_runner/exgentic_a2a_runner/runner.py @@ -215,9 +215,9 @@ def process_session(self, session_data: SessionData) -> SessionResult: eval_duration_ms = (time.time() - eval_start) * 1000 self.otel.record_evaluation(span, eval_duration_ms) - # Close session - with self.otel.child_span("exgentic_a2a.mcp.close_session"): - self.exgentic.close_session(session_id) + # Delete session + with self.otel.child_span("exgentic_a2a.mcp.delete_session"): + self.exgentic.delete_session(session_id) # Record success self.otel.record_success(span, evaluation_result) @@ -243,12 +243,12 @@ def process_session(self, session_data: SessionData) -> SessionResult: logger.error(f"Session {session_id} failed: {error_type}: {error_msg}") - # Try to close session even on failure + # Try to delete session even on failure try: - with self.otel.child_span("exgentic_a2a.mcp.close_session"): - self.exgentic.close_session(session_id) - except Exception as close_error: - logger.warning(f"Failed to close session {session_id}: {close_error}") + with self.otel.child_span("exgentic_a2a.mcp.delete_session"): + self.exgentic.delete_session(session_id) + except Exception as delete_error: + logger.warning(f"Failed to delete session {session_id}: {delete_error}") # Record failure self.otel.record_failure(span, e, error_type) diff --git a/exgentic_a2a_runner/run-with-port-forward.sh b/exgentic_a2a_runner/run-with-port-forward.sh index b1ea642..4d35699 100755 --- a/exgentic_a2a_runner/run-with-port-forward.sh +++ b/exgentic_a2a_runner/run-with-port-forward.sh @@ -46,6 +46,36 @@ lsof -ti:8000 | xargs kill -9 2>/dev/null || true lsof -ti:8081 | xargs kill -9 2>/dev/null || true sleep 2 +# Check if pods are ready before port-forwarding +echo "Checking if pods are ready..." + +# Extract deployment names (remove -mcp suffix from BENCHMARK_SERVICE if present) +BENCHMARK_DEPLOYMENT="${BENCHMARK_SERVICE%-mcp}" +AGENT_DEPLOYMENT="$AGENT_SERVICE" + +# Wait for MCP server pod to be ready +echo " Checking MCP server pod..." +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=$BENCHMARK_DEPLOYMENT -n team1 --timeout=60s +if [ $? -ne 0 ]; then + echo "Error: MCP server pod is not ready" + exit 1 +fi + +# Wait for agent pod to be ready +echo " Checking agent pod..." +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=$AGENT_DEPLOYMENT -n team1 --timeout=60s +if [ $? -ne 0 ]; then + echo "Error: Agent pod is not ready" + exit 1 +fi + +echo "✓ All pods are ready" +echo "" + +# Additional wait to ensure services are fully started +echo "Waiting for services to be fully started..." +sleep 10 + # Start port forwarding in background echo "Starting port-forward for MCP server..." kubectl port-forward -n team1 svc/$BENCHMARK_SERVICE 8000:8000 & @@ -57,7 +87,7 @@ PF_AGENT_PID=$! # Wait for port forwards to be ready echo "Waiting for port forwards to be ready..." -sleep 3 +sleep 5 # Check if port forwards are working if ! ps -p $PF_MCP_PID > /dev/null; then