From 0114f5a4f36c3e313d1affeb4cfe503848c87134 Mon Sep 17 00:00:00 2001 From: Amber Agent Date: Tue, 27 Jan 2026 04:58:18 +0000 Subject: [PATCH] chore(runner): update claude-agent-sdk to v0.1.23 Update claude-agent-sdk from v0.1.12 to v0.1.23 (latest version). This update brings the runner component up to date with the latest SDK features and fixes. Also fixed code formatting and import ordering across the runner codebase: - Applied black formatting to all Python files - Fixed import ordering with isort (black profile) Changes: - pyproject.toml: Updated claude-agent-sdk>=0.1.12 to >=0.1.23 - uv.lock: Regenerated lock file with updated dependencies - All Python files: Applied black formatting and isort Tests: 81/83 tests passing (2 pre-existing async test config failures) Co-Authored-By: Claude Sonnet 4.5 --- .../runners/claude-code-runner/adapter.py | 796 +++++---- .../runners/claude-code-runner/context.py | 3 +- components/runners/claude-code-runner/main.py | 714 +++++--- .../claude-code-runner/observability.py | 158 +- .../runners/claude-code-runner/pyproject.toml | 2 +- .../claude-code-runner/security_utils.py | 6 +- .../tests/test_auto_push.py | 180 +- .../tests/test_duplicate_turn_prevention.py | 12 +- .../tests/test_langfuse_model_metadata.py | 77 +- .../tests/test_model_mapping.py | 171 +- .../tests/test_observability.py | 29 +- .../tests/test_privacy_masking.py | 37 +- .../tests/test_security_utils.py | 42 +- .../tests/test_wrapper_vertex.py | 277 +-- components/runners/claude-code-runner/uv.lock | 1583 +++++++++++++++-- 15 files changed, 2983 insertions(+), 1104 deletions(-) diff --git a/components/runners/claude-code-runner/adapter.py b/components/runners/claude-code-runner/adapter.py index 8d73b4532..61258f5f0 100644 --- a/components/runners/claude-code-runner/adapter.py +++ b/components/runners/claude-code-runner/adapter.py @@ -8,41 +8,42 @@ """ import asyncio -import os -import sys -import logging import json as _json +import logging +import os import re import shutil +import sys import uuid +from datetime import datetime, timezone from pathlib import Path -from typing import AsyncIterator, Optional, Any +from typing import Any, AsyncIterator, Optional +from urllib import error as _urllib_error +from urllib import request as _urllib_request from urllib.parse import urlparse, urlunparse -from urllib import request as _urllib_request, error as _urllib_error -from datetime import datetime, timezone # Set umask to make files readable by content service container os.umask(0o022) # AG-UI Protocol Events from ag_ui.core import ( + BaseEvent, EventType, + RawEvent, RunAgentInput, - BaseEvent, - RunStartedEvent, - RunFinishedEvent, RunErrorEvent, - TextMessageStartEvent, + RunFinishedEvent, + RunStartedEvent, + StateDeltaEvent, + StateSnapshotEvent, + StepFinishedEvent, + StepStartedEvent, TextMessageContentEvent, TextMessageEndEvent, - ToolCallStartEvent, + TextMessageStartEvent, ToolCallArgsEvent, ToolCallEndEvent, - StepStartedEvent, - StepFinishedEvent, - StateSnapshotEvent, - StateDeltaEvent, - RawEvent, + ToolCallStartEvent, ) from context import RunnerContext @@ -52,13 +53,14 @@ class PrerequisiteError(RuntimeError): """Raised when slash-command prerequisites are missing.""" + pass class ClaudeCodeAdapter: """ Adapter that wraps the Claude Code SDK for AG-UI server. - + Produces AG-UI events via async generator instead of WebSocket. """ @@ -75,7 +77,7 @@ def __init__(self): # in _run_claude_agent_sdk to avoid race conditions with concurrent runs self._current_run_id: Optional[str] = None self._current_thread_id: Optional[str] = None - + # Active client reference for interrupt support self._active_client: Optional[Any] = None @@ -86,19 +88,21 @@ async def initialize(self, context: RunnerContext): # Copy Google OAuth credentials from mounted Secret to writable workspace location await self._setup_google_credentials() - + # Workspace is already prepared by init container (hydrate.sh) # - Repos cloned to /workspace/repos/ # - Workflows cloned to /workspace/workflows/ # - State hydrated from S3 to .claude/, artifacts/, file-uploads/ logger.info("Workspace prepared by init container, validating...") - + # Validate prerequisite files exist for phase-based commands try: await self._validate_prerequisites() except PrerequisiteError as exc: self.last_exit_code = 2 - logger.error("Prerequisite validation failed during initialization: %s", exc) + logger.error( + "Prerequisite validation failed during initialization: %s", exc + ) raise def _timestamp(self) -> str: @@ -108,26 +112,26 @@ def _timestamp(self) -> str: async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEvent]: """ Process a run and yield AG-UI events. - + This is the main entry point called by the FastAPI server. - + Args: input_data: RunAgentInput with thread_id, run_id, messages, tools app_state: Optional FastAPI app.state for persistent client storage/reuse - + Yields: AG-UI events (RunStartedEvent, TextMessageContentEvent, etc.) """ thread_id = input_data.thread_id or self.context.session_id run_id = input_data.run_id or str(uuid.uuid4()) - + self._current_thread_id = thread_id self._current_run_id = run_id - + # Check for newly available Google OAuth credentials (user may have authenticated mid-session) # This picks up credentials after K8s syncs the mounted secret (~60s after OAuth completes) await self.refresh_google_credentials() - + try: # Emit RUN_STARTED yield RunStartedEvent( @@ -135,22 +139,30 @@ async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEven thread_id=thread_id, run_id=run_id, ) - + # Echo user messages as events (for history/display) for msg in input_data.messages or []: - msg_dict = msg if isinstance(msg, dict) else (msg.model_dump() if hasattr(msg, 'model_dump') else {}) - role = msg_dict.get('role', '') - - if role == 'user': - msg_id = msg_dict.get('id', str(uuid.uuid4())) - content = msg_dict.get('content', '') - msg_metadata = msg_dict.get('metadata', {}) - + msg_dict = ( + msg + if isinstance(msg, dict) + else (msg.model_dump() if hasattr(msg, "model_dump") else {}) + ) + role = msg_dict.get("role", "") + + if role == "user": + msg_id = msg_dict.get("id", str(uuid.uuid4())) + content = msg_dict.get("content", "") + msg_metadata = msg_dict.get("metadata", {}) + # Check if message should be hidden from UI - is_hidden = isinstance(msg_metadata, dict) and msg_metadata.get('hidden', False) + is_hidden = isinstance(msg_metadata, dict) and msg_metadata.get( + "hidden", False + ) if is_hidden: - logger.info(f"Message {msg_id[:8]} marked as hidden (auto-sent initial/workflow prompt)") - + logger.info( + f"Message {msg_id[:8]} marked as hidden (auto-sent initial/workflow prompt)" + ) + # Emit user message as TEXT_MESSAGE events # Include metadata in RAW event for frontend filtering if is_hidden: @@ -163,17 +175,17 @@ async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEven "messageId": msg_id, "metadata": msg_metadata, "hidden": True, - } + }, ) - + yield TextMessageStartEvent( type=EventType.TEXT_MESSAGE_START, thread_id=thread_id, run_id=run_id, message_id=msg_id, - role='user', + role="user", ) - + if content: yield TextMessageContentEvent( type=EventType.TEXT_MESSAGE_CONTENT, @@ -182,26 +194,30 @@ async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEven message_id=msg_id, delta=content, ) - + yield TextMessageEndEvent( type=EventType.TEXT_MESSAGE_END, thread_id=thread_id, run_id=run_id, message_id=msg_id, ) - + # Extract user message from input - logger.info(f"Extracting user message from {len(input_data.messages)} messages") + logger.info( + f"Extracting user message from {len(input_data.messages)} messages" + ) user_message = self._extract_user_message(input_data) - logger.info(f"Extracted user message: '{user_message[:100] if user_message else '(empty)'}...'") - + logger.info( + f"Extracted user message: '{user_message[:100] if user_message else '(empty)'}...'" + ) + if not user_message: logger.warning("No user message found in input") yield RawEvent( type=EventType.RAW, thread_id=thread_id, run_id=run_id, - event={"type": "system_log", "message": "No user message provided"} + event={"type": "system_log", "message": "No user message provided"}, ) yield RunFinishedEvent( type=EventType.RUN_FINISHED, @@ -209,22 +225,24 @@ async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEven run_id=run_id, ) return - + # Run Claude SDK and yield events logger.info(f"Starting Claude SDK with prompt: '{user_message[:50]}...'") - async for event in self._run_claude_agent_sdk(user_message, thread_id, run_id): + async for event in self._run_claude_agent_sdk( + user_message, thread_id, run_id + ): yield event logger.info(f"Claude SDK processing completed for run {run_id}") - + # Emit RUN_FINISHED yield RunFinishedEvent( type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id, ) - + self.last_exit_code = 0 - + except PrerequisiteError as e: self.last_exit_code = 2 logger.error(f"Prerequisite validation failed: {e}") @@ -247,33 +265,43 @@ async def process_run(self, input_data: RunAgentInput) -> AsyncIterator[BaseEven def _extract_user_message(self, input_data: RunAgentInput) -> str: """Extract user message text from RunAgentInput.""" messages = input_data.messages or [] - logger.info(f"Extracting from {len(messages)} messages, types: {[type(m).__name__ for m in messages]}") - + logger.info( + f"Extracting from {len(messages)} messages, types: {[type(m).__name__ for m in messages]}" + ) + # Find the last user message for msg in reversed(messages): - logger.debug(f"Checking message: type={type(msg).__name__}, hasattr(role)={hasattr(msg, 'role')}") - - if hasattr(msg, 'role') and msg.role == 'user': + logger.debug( + f"Checking message: type={type(msg).__name__}, hasattr(role)={hasattr(msg, 'role')}" + ) + + if hasattr(msg, "role") and msg.role == "user": # Handle different content formats - content = getattr(msg, 'content', '') + content = getattr(msg, "content", "") if isinstance(content, str): - logger.info(f"Found user message (object format): '{content[:50]}...'") + logger.info( + f"Found user message (object format): '{content[:50]}...'" + ) return content elif isinstance(content, list): # Content blocks format for block in content: - if hasattr(block, 'text'): + if hasattr(block, "text"): return block.text - elif isinstance(block, dict) and 'text' in block: - return block['text'] + elif isinstance(block, dict) and "text" in block: + return block["text"] elif isinstance(msg, dict): - logger.debug(f"Dict message: role={msg.get('role')}, content={msg.get('content', '')[:30]}...") - if msg.get('role') == 'user': - content = msg.get('content', '') + logger.debug( + f"Dict message: role={msg.get('role')}, content={msg.get('content', '')[:30]}..." + ) + if msg.get("role") == "user": + content = msg.get("content", "") if isinstance(content, str): - logger.info(f"Found user message (dict format): '{content[:50]}...'") + logger.info( + f"Found user message (dict format): '{content[:50]}...'" + ) return content - + logger.warning("No user message found!") return "" @@ -281,9 +309,9 @@ async def _run_claude_agent_sdk( self, prompt: str, thread_id: str, run_id: str ) -> AsyncIterator[BaseEvent]: """Execute the Claude Code SDK with the given prompt and yield AG-UI events. - + Creates a fresh client for each run - simpler and more reliable than client reuse. - + Args: prompt: The user prompt to send to Claude thread_id: AG-UI thread identifier @@ -291,81 +319,95 @@ async def _run_claude_agent_sdk( """ # Per-run state - NOT instance variables to avoid race conditions with concurrent runs current_message_id: Optional[str] = None - - logger.info(f"_run_claude_agent_sdk called with prompt length={len(prompt)}, will create fresh client") + + logger.info( + f"_run_claude_agent_sdk called with prompt length={len(prompt)}, will create fresh client" + ) try: # Check for authentication method logger.info("Checking authentication configuration...") - api_key = self.context.get_env('ANTHROPIC_API_KEY', '') - use_vertex = self.context.get_env('CLAUDE_CODE_USE_VERTEX', '').strip() == '1' - - logger.info(f"Auth config: api_key={'set' if api_key else 'not set'}, use_vertex={use_vertex}") + api_key = self.context.get_env("ANTHROPIC_API_KEY", "") + use_vertex = ( + self.context.get_env("CLAUDE_CODE_USE_VERTEX", "").strip() == "1" + ) + + logger.info( + f"Auth config: api_key={'set' if api_key else 'not set'}, use_vertex={use_vertex}" + ) if not api_key and not use_vertex: - raise RuntimeError("Either ANTHROPIC_API_KEY or CLAUDE_CODE_USE_VERTEX=1 must be set") + raise RuntimeError( + "Either ANTHROPIC_API_KEY or CLAUDE_CODE_USE_VERTEX=1 must be set" + ) # Set environment variables BEFORE importing SDK if api_key: - os.environ['ANTHROPIC_API_KEY'] = api_key + os.environ["ANTHROPIC_API_KEY"] = api_key logger.info("Using Anthropic API key authentication") # Configure Vertex AI if requested if use_vertex: vertex_credentials = await self._setup_vertex_credentials() - if 'ANTHROPIC_API_KEY' in os.environ: + if "ANTHROPIC_API_KEY" in os.environ: logger.info("Clearing ANTHROPIC_API_KEY to force Vertex AI mode") - del os.environ['ANTHROPIC_API_KEY'] + del os.environ["ANTHROPIC_API_KEY"] - os.environ['CLAUDE_CODE_USE_VERTEX'] = '1' - os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = vertex_credentials.get('credentials_path', '') - os.environ['ANTHROPIC_VERTEX_PROJECT_ID'] = vertex_credentials.get('project_id', '') - os.environ['CLOUD_ML_REGION'] = vertex_credentials.get('region', '') + os.environ["CLAUDE_CODE_USE_VERTEX"] = "1" + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = vertex_credentials.get( + "credentials_path", "" + ) + os.environ["ANTHROPIC_VERTEX_PROJECT_ID"] = vertex_credentials.get( + "project_id", "" + ) + os.environ["CLOUD_ML_REGION"] = vertex_credentials.get("region", "") # NOW we can safely import the SDK - from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions from claude_agent_sdk import ( AssistantMessage, - UserMessage, - SystemMessage, + ClaudeAgentOptions, + ClaudeSDKClient, ResultMessage, + SystemMessage, TextBlock, ThinkingBlock, - ToolUseBlock, ToolResultBlock, + ToolUseBlock, + UserMessage, + create_sdk_mcp_server, ) + from claude_agent_sdk import tool as sdk_tool from claude_agent_sdk.types import StreamEvent - from claude_agent_sdk import tool as sdk_tool, create_sdk_mcp_server from observability import ObservabilityManager # Extract and sanitize user context for observability - raw_user_id = os.getenv('USER_ID', '').strip() - raw_user_name = os.getenv('USER_NAME', '').strip() + raw_user_id = os.getenv("USER_ID", "").strip() + raw_user_name = os.getenv("USER_NAME", "").strip() user_id, user_name = self._sanitize_user_context(raw_user_id, raw_user_name) # Get model configuration - model = self.context.get_env('LLM_MODEL') - configured_model = model or 'claude-sonnet-4-5@20250929' + model = self.context.get_env("LLM_MODEL") + configured_model = model or "claude-sonnet-4-5@20250929" if use_vertex and model: configured_model = self._map_to_vertex_model(model) # Initialize observability obs = ObservabilityManager( - session_id=self.context.session_id, - user_id=user_id, - user_name=user_name + session_id=self.context.session_id, user_id=user_id, user_name=user_name ) await obs.initialize( prompt=prompt, - namespace=self.context.get_env('AGENTIC_SESSION_NAMESPACE', 'unknown'), - model=configured_model + namespace=self.context.get_env("AGENTIC_SESSION_NAMESPACE", "unknown"), + model=configured_model, ) obs._pending_initial_prompt = prompt # Check if this is a resume session via IS_RESUME env var # This is set by the operator when restarting a stopped/completed/failed session - is_continuation = self.context.get_env('IS_RESUME', '').strip().lower() == 'true' + is_continuation = ( + self.context.get_env("IS_RESUME", "").strip().lower() == "true" + ) if is_continuation: logger.info("IS_RESUME=true - treating as continuation") @@ -376,7 +418,7 @@ async def _run_claude_agent_sdk( derived_name = None # Check for active workflow first - active_workflow_url = (os.getenv('ACTIVE_WORKFLOW_GIT_URL') or '').strip() + active_workflow_url = (os.getenv("ACTIVE_WORKFLOW_GIT_URL") or "").strip() if active_workflow_url: cwd_path, add_dirs, derived_name = self._setup_workflow_paths( active_workflow_url, repos_cfg @@ -387,12 +429,16 @@ async def _run_claude_agent_sdk( cwd_path = str(Path(self.context.workspace_path) / "artifacts") # Load ambient.json configuration - ambient_config = self._load_ambient_config(cwd_path) if active_workflow_url else {} + ambient_config = ( + self._load_ambient_config(cwd_path) if active_workflow_url else {} + ) # Ensure working directory exists cwd_path_obj = Path(cwd_path) if not cwd_path_obj.exists(): - logger.warning(f"Working directory does not exist, creating: {cwd_path}") + logger.warning( + f"Working directory does not exist, creating: {cwd_path}" + ) try: cwd_path_obj.mkdir(parents=True, exist_ok=True) except Exception as e: @@ -404,45 +450,60 @@ async def _run_claude_agent_sdk( # Load MCP server configuration (webfetch is included in static .mcp.json) mcp_servers = self._load_mcp_config(cwd_path) or {} - + # Create custom session control tools # Capture self reference for the restart tool closure adapter_ref = self - - @sdk_tool("restart_session", "Restart the Claude session to recover from issues, clear state, or get a fresh connection. Use this if you detect you're in a broken state or need to reset.", {}) + + @sdk_tool( + "restart_session", + "Restart the Claude session to recover from issues, clear state, or get a fresh connection. Use this if you detect you're in a broken state or need to reset.", + {}, + ) async def restart_session_tool(args: dict) -> dict: """Tool that allows Claude to request a session restart.""" adapter_ref._restart_requested = True logger.info("🔄 Session restart requested by Claude via MCP tool") return { - "content": [{ - "type": "text", - "text": "Session restart has been requested. The current run will complete and a fresh session will be established. Your conversation context will be preserved on disk." - }] + "content": [ + { + "type": "text", + "text": "Session restart has been requested. The current run will complete and a fresh session will be established. Your conversation context will be preserved on disk.", + } + ] } - + # Create SDK MCP server for session tools session_tools_server = create_sdk_mcp_server( - name="session", - version="1.0.0", - tools=[restart_session_tool] + name="session", version="1.0.0", tools=[restart_session_tool] ) mcp_servers["session"] = session_tools_server logger.info("Added custom session control MCP tools (restart_session)") - + # Disable built-in WebFetch in favor of WebFetch.MCP from config - allowed_tools = ["Read", "Write", "Bash", "Glob", "Grep", "Edit", "MultiEdit", "WebSearch"] + allowed_tools = [ + "Read", + "Write", + "Bash", + "Glob", + "Grep", + "Edit", + "MultiEdit", + "WebSearch", + ] if mcp_servers: for server_name in mcp_servers.keys(): allowed_tools.append(f"mcp__{server_name}") - logger.info(f"MCP tool permissions granted for servers: {list(mcp_servers.keys())}") + logger.info( + f"MCP tool permissions granted for servers: {list(mcp_servers.keys())}" + ) # Build workspace context system prompt workspace_prompt = self._build_workspace_context_prompt( repos_cfg=repos_cfg, workflow_name=derived_name if active_workflow_url else None, artifacts_path="artifacts", - ambient_config=ambient_config + ambient_config=ambient_config, ) system_prompt_config = {"type": "text", "text": workspace_prompt} @@ -473,14 +534,18 @@ async def restart_session_tool(args: dict) -> dict: except Exception: pass - max_tokens_env = self.context.get_env('LLM_MAX_TOKENS') or self.context.get_env('MAX_TOKENS') + max_tokens_env = self.context.get_env( + "LLM_MAX_TOKENS" + ) or self.context.get_env("MAX_TOKENS") if max_tokens_env: try: options.max_tokens = int(max_tokens_env) except Exception: pass - temperature_env = self.context.get_env('LLM_TEMPERATURE') or self.context.get_env('TEMPERATURE') + temperature_env = self.context.get_env( + "LLM_TEMPERATURE" + ) or self.context.get_env("TEMPERATURE") if temperature_env: try: options.temperature = float(temperature_env) @@ -492,28 +557,33 @@ async def restart_session_tool(args: dict) -> dict: sdk_session_id = None def create_sdk_client(opts, disable_continue=False): - if disable_continue and hasattr(opts, 'continue_conversation'): + if disable_continue and hasattr(opts, "continue_conversation"): opts.continue_conversation = False return ClaudeSDKClient(options=opts) # Create fresh client for each run # (Python SDK has issues with client reuse despite docs suggesting it should work) logger.info("Creating new ClaudeSDKClient for this run...") - + # Enable continue_conversation to resume from disk state if not self._first_run or is_continuation: try: options.continue_conversation = True - logger.info("Enabled continue_conversation (will resume from disk state)") + logger.info( + "Enabled continue_conversation (will resume from disk state)" + ) yield RawEvent( type=EventType.RAW, thread_id=thread_id, run_id=run_id, - event={"type": "system_log", "message": "🔄 Resuming conversation from disk state"} + event={ + "type": "system_log", + "message": "🔄 Resuming conversation from disk state", + }, ) except Exception as e: logger.warning(f"Failed to set continue_conversation: {e}") - + try: logger.info("Creating ClaudeSDKClient...") client = create_sdk_client(options) @@ -528,7 +598,10 @@ def create_sdk_client(opts, disable_continue=False): type=EventType.RAW, thread_id=thread_id, run_id=run_id, - event={"type": "system_log", "message": "⚠️ Could not continue conversation, starting fresh..."} + event={ + "type": "system_log", + "message": "⚠️ Could not continue conversation, starting fresh...", + }, ) client = create_sdk_client(options, disable_continue=True) await client.connect() @@ -556,17 +629,19 @@ def create_sdk_client(opts, disable_continue=False): # Process response stream logger.info("Starting to consume receive_response() iterator...") message_count = 0 - + async for message in client.receive_response(): message_count += 1 - logger.info(f"[ClaudeSDKClient Message #{message_count}]: {message}") + logger.info( + f"[ClaudeSDKClient Message #{message_count}]: {message}" + ) # Handle StreamEvent for real-time streaming chunks if isinstance(message, StreamEvent): event_data = message.event - event_type = event_data.get('type') + event_type = event_data.get("type") - if event_type == 'message_start': + if event_type == "message_start": current_message_id = str(uuid.uuid4()) yield TextMessageStartEvent( type=EventType.TEXT_MESSAGE_START, @@ -576,10 +651,10 @@ def create_sdk_client(opts, disable_continue=False): role="assistant", ) - elif event_type == 'content_block_delta': - delta_data = event_data.get('delta', {}) - if delta_data.get('type') == 'text_delta': - text_chunk = delta_data.get('text', '') + elif event_type == "content_block_delta": + delta_data = event_data.get("delta", {}) + if delta_data.get("type") == "text_delta": + text_chunk = delta_data.get("text", "") if text_chunk and current_message_id: yield TextMessageContentEvent( type=EventType.TEXT_MESSAGE_CONTENT, @@ -592,15 +667,15 @@ def create_sdk_client(opts, disable_continue=False): # Capture SDK session ID from init message if isinstance(message, SystemMessage): - if message.subtype == 'init' and message.data.get('session_id'): - sdk_session_id = message.data.get('session_id') + if message.subtype == "init" and message.data.get("session_id"): + sdk_session_id = message.data.get("session_id") logger.info(f"Captured SDK session ID: {sdk_session_id}") if isinstance(message, (AssistantMessage, UserMessage)): if isinstance(message, AssistantMessage): current_message = message obs.start_turn(configured_model, user_input=prompt) - + # Emit trace_id for feedback association # Frontend can use this to link feedback to specific Langfuse traces trace_id = obs.get_current_trace_id() @@ -612,23 +687,31 @@ def create_sdk_client(opts, disable_continue=False): event={ "type": "langfuse_trace", "traceId": trace_id, - } + }, ) # Process all blocks in the message - for block in getattr(message, 'content', []) or []: + for block in getattr(message, "content", []) or []: if isinstance(block, TextBlock): - text_piece = getattr(block, 'text', None) + text_piece = getattr(block, "text", None) if text_piece: - logger.info(f"TextBlock received (complete), text length={len(text_piece)}") + logger.info( + f"TextBlock received (complete), text length={len(text_piece)}" + ) elif isinstance(block, ToolUseBlock): - tool_name = getattr(block, 'name', '') or 'unknown' - tool_input = getattr(block, 'input', {}) or {} - tool_id = getattr(block, 'id', None) or str(uuid.uuid4()) - parent_tool_use_id = getattr(message, 'parent_tool_use_id', None) + tool_name = getattr(block, "name", "") or "unknown" + tool_input = getattr(block, "input", {}) or {} + tool_id = getattr(block, "id", None) or str( + uuid.uuid4() + ) + parent_tool_use_id = getattr( + message, "parent_tool_use_id", None + ) - logger.info(f"ToolUseBlock detected: {tool_name} (id={tool_id[:12]})") + logger.info( + f"ToolUseBlock detected: {tool_name} (id={tool_id[:12]})" + ) yield ToolCallStartEvent( type=EventType.TOOL_CALL_START, @@ -652,11 +735,13 @@ def create_sdk_client(opts, disable_continue=False): obs.track_tool_use(tool_name, tool_id, tool_input) elif isinstance(block, ToolResultBlock): - tool_use_id = getattr(block, 'tool_use_id', None) - content = getattr(block, 'content', None) - is_error = getattr(block, 'is_error', None) - result_text = getattr(block, 'text', None) - result_content = content if content is not None else result_text + tool_use_id = getattr(block, "tool_use_id", None) + content = getattr(block, "content", None) + is_error = getattr(block, "is_error", None) + result_text = getattr(block, "text", None) + result_content = ( + content if content is not None else result_text + ) if result_content is not None: try: @@ -676,11 +761,13 @@ def create_sdk_client(opts, disable_continue=False): error=result_str if is_error else None, ) - obs.track_tool_result(tool_use_id, result_content, is_error or False) + obs.track_tool_result( + tool_use_id, result_content, is_error or False + ) elif isinstance(block, ThinkingBlock): - thinking_text = getattr(block, 'thinking', '') - signature = getattr(block, 'signature', '') + thinking_text = getattr(block, "thinking", "") + signature = getattr(block, "signature", "") yield RawEvent( type=EventType.RAW, thread_id=thread_id, @@ -689,11 +776,11 @@ def create_sdk_client(opts, disable_continue=False): "type": "thinking_block", "thinking": thinking_text, "signature": signature, - } + }, ) # End text message after processing all blocks - if getattr(message, 'content', []) and current_message_id: + if getattr(message, "content", []) and current_message_id: yield TextMessageEndEvent( type=EventType.TEXT_MESSAGE_END, thread_id=thread_id, @@ -703,48 +790,63 @@ def create_sdk_client(opts, disable_continue=False): current_message_id = None elif isinstance(message, SystemMessage): - text = getattr(message, 'text', None) + text = getattr(message, "text", None) if text: yield RawEvent( type=EventType.RAW, thread_id=thread_id, run_id=run_id, - event={"type": "system_log", "level": "debug", "message": str(text)} + event={ + "type": "system_log", + "level": "debug", + "message": str(text), + }, ) elif isinstance(message, ResultMessage): - usage_raw = getattr(message, 'usage', None) - sdk_num_turns = getattr(message, 'num_turns', None) + usage_raw = getattr(message, "usage", None) + sdk_num_turns = getattr(message, "num_turns", None) - logger.info(f"ResultMessage: num_turns={sdk_num_turns}, usage={usage_raw}") + logger.info( + f"ResultMessage: num_turns={sdk_num_turns}, usage={usage_raw}" + ) # Convert usage object to dict if needed if usage_raw is not None and not isinstance(usage_raw, dict): try: - if hasattr(usage_raw, '__dict__'): + if hasattr(usage_raw, "__dict__"): usage_raw = usage_raw.__dict__ - elif hasattr(usage_raw, 'model_dump'): + elif hasattr(usage_raw, "model_dump"): usage_raw = usage_raw.model_dump() except Exception as e: - logger.warning(f"Could not convert usage object to dict: {e}") + logger.warning( + f"Could not convert usage object to dict: {e}" + ) # Update turn count - if sdk_num_turns is not None and sdk_num_turns > self._turn_count: + if ( + sdk_num_turns is not None + and sdk_num_turns > self._turn_count + ): self._turn_count = sdk_num_turns # Complete turn tracking if current_message: - obs.end_turn(self._turn_count, current_message, usage_raw if isinstance(usage_raw, dict) else None) + obs.end_turn( + self._turn_count, + current_message, + usage_raw if isinstance(usage_raw, dict) else None, + ) current_message = None result_payload = { - "subtype": getattr(message, 'subtype', None), - "duration_ms": getattr(message, 'duration_ms', None), - "is_error": getattr(message, 'is_error', None), - "num_turns": getattr(message, 'num_turns', None), - "total_cost_usd": getattr(message, 'total_cost_usd', None), + "subtype": getattr(message, "subtype", None), + "duration_ms": getattr(message, "duration_ms", None), + "is_error": getattr(message, "is_error", None), + "num_turns": getattr(message, "num_turns", None), + "total_cost_usd": getattr(message, "total_cost_usd", None), "usage": usage_raw, - "result": getattr(message, 'result', None), + "result": getattr(message, "result", None), } # Emit state delta with result @@ -752,7 +854,13 @@ def create_sdk_client(opts, disable_continue=False): type=EventType.STATE_DELTA, thread_id=thread_id, run_id=run_id, - delta=[{"op": "replace", "path": "/lastResult", "value": result_payload}], + delta=[ + { + "op": "replace", + "path": "/lastResult", + "value": result_payload, + } + ], ) # End step @@ -763,12 +871,14 @@ def create_sdk_client(opts, disable_continue=False): step_id=step_id, step_name="processing_prompt", ) - - logger.info(f"Response iterator fully consumed ({message_count} messages total)") + + logger.info( + f"Response iterator fully consumed ({message_count} messages total)" + ) # Mark first run complete self._first_run = False - + # Check if restart was requested by Claude if self._restart_requested: logger.info("🔄 Restart was requested, emitting restart event") @@ -779,28 +889,28 @@ def create_sdk_client(opts, disable_continue=False): run_id=run_id, event={ "type": "session_restart_requested", - "message": "Claude requested a session restart. Reconnecting..." - } + "message": "Claude requested a session restart. Reconnecting...", + }, ) finally: # Clear active client reference self._active_client = None - + # Always disconnect client at end of run if client is not None: logger.info("Disconnecting client (end of run)") await client.disconnect() - + # Finalize observability await obs.finalize() except Exception as e: logger.error(f"Failed to run Claude Code SDK: {e}") - if 'obs' in locals(): + if "obs" in locals(): await obs.cleanup_on_error(e) raise - + async def interrupt(self) -> None: """ Interrupt the active Claude SDK execution. @@ -808,7 +918,7 @@ async def interrupt(self) -> None: if self._active_client is None: logger.warning("Interrupt requested but no active client") return - + try: logger.info("Sending interrupt signal to Claude SDK client...") await self._active_client.interrupt() @@ -816,7 +926,9 @@ async def interrupt(self) -> None: except Exception as e: logger.error(f"Failed to interrupt Claude SDK: {e}") - def _setup_workflow_paths(self, active_workflow_url: str, repos_cfg: list) -> tuple[str, list, str]: + def _setup_workflow_paths( + self, active_workflow_url: str, repos_cfg: list + ) -> tuple[str, list, str]: """Setup paths for workflow mode.""" add_dirs = [] derived_name = None @@ -824,24 +936,32 @@ def _setup_workflow_paths(self, active_workflow_url: str, repos_cfg: list) -> tu try: owner, repo, _ = self._parse_owner_repo(active_workflow_url) - derived_name = repo or '' + derived_name = repo or "" if not derived_name: p = urlparse(active_workflow_url) - parts = [pt for pt in (p.path or '').split('/') if pt] + parts = [pt for pt in (p.path or "").split("/") if pt] if parts: derived_name = parts[-1] - derived_name = (derived_name or '').removesuffix('.git').strip() + derived_name = (derived_name or "").removesuffix(".git").strip() if derived_name: - workflow_path = str(Path(self.context.workspace_path) / "workflows" / derived_name) + workflow_path = str( + Path(self.context.workspace_path) / "workflows" / derived_name + ) if Path(workflow_path).exists(): cwd_path = workflow_path logger.info(f"Using workflow as CWD: {derived_name}") else: - logger.warning(f"Workflow directory not found: {workflow_path}, using default") - cwd_path = str(Path(self.context.workspace_path) / "workflows" / "default") + logger.warning( + f"Workflow directory not found: {workflow_path}, using default" + ) + cwd_path = str( + Path(self.context.workspace_path) / "workflows" / "default" + ) else: - cwd_path = str(Path(self.context.workspace_path) / "workflows" / "default") + cwd_path = str( + Path(self.context.workspace_path) / "workflows" / "default" + ) except Exception as e: logger.warning(f"Failed to derive workflow name: {e}, using default") cwd_path = str(Path(self.context.workspace_path) / "workflows" / "default") @@ -849,7 +969,7 @@ def _setup_workflow_paths(self, active_workflow_url: str, repos_cfg: list) -> tu # Add all repos as additional directories (repos are in /workspace/repos/{name}) repos_base = Path(self.context.workspace_path) / "repos" for r in repos_cfg: - name = (r.get('name') or '').strip() + name = (r.get("name") or "").strip() if name: repo_path = str(repos_base / name) if repo_path not in add_dirs: @@ -868,30 +988,32 @@ def _setup_workflow_paths(self, active_workflow_url: str, repos_cfg: list) -> tu def _setup_multi_repo_paths(self, repos_cfg: list) -> tuple[str, list]: """Setup paths for multi-repo mode. - + Repos are cloned to /workspace/repos/{name} by both: - hydrate.sh (init container) - clone_repo_at_runtime() (runtime addition) """ add_dirs = [] repos_base = Path(self.context.workspace_path) / "repos" - - main_name = (os.getenv('MAIN_REPO_NAME') or '').strip() + + main_name = (os.getenv("MAIN_REPO_NAME") or "").strip() if not main_name: - idx_raw = (os.getenv('MAIN_REPO_INDEX') or '').strip() + idx_raw = (os.getenv("MAIN_REPO_INDEX") or "").strip() try: idx_val = int(idx_raw) if idx_raw else 0 except Exception: idx_val = 0 if idx_val < 0 or idx_val >= len(repos_cfg): idx_val = 0 - main_name = (repos_cfg[idx_val].get('name') or '').strip() + main_name = (repos_cfg[idx_val].get("name") or "").strip() # Main repo path is /workspace/repos/{name} - cwd_path = str(repos_base / main_name) if main_name else self.context.workspace_path + cwd_path = ( + str(repos_base / main_name) if main_name else self.context.workspace_path + ) for r in repos_cfg: - name = (r.get('name') or '').strip() + name = (r.get("name") or "").strip() if not name: continue # All repos are in /workspace/repos/{name} @@ -917,14 +1039,14 @@ def _sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: user_id = str(user_id).strip() if len(user_id) > 255: user_id = user_id[:255] - sanitized_id = re.sub(r'[^a-zA-Z0-9@._-]', '', user_id) + sanitized_id = re.sub(r"[^a-zA-Z0-9@._-]", "", user_id) user_id = sanitized_id if user_name: user_name = str(user_name).strip() if len(user_name) > 255: user_name = user_name[:255] - sanitized_name = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', user_name) + sanitized_name = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", user_name) user_name = sanitized_name return user_id, user_name @@ -932,49 +1054,59 @@ def _sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: def _map_to_vertex_model(self, model: str) -> str: """Map Anthropic API model names to Vertex AI model names.""" model_map = { - 'claude-opus-4-5': 'claude-opus-4-5@20251101', - 'claude-opus-4-1': 'claude-opus-4-1@20250805', - 'claude-sonnet-4-5': 'claude-sonnet-4-5@20250929', - 'claude-haiku-4-5': 'claude-haiku-4-5@20251001', + "claude-opus-4-5": "claude-opus-4-5@20251101", + "claude-opus-4-1": "claude-opus-4-1@20250805", + "claude-sonnet-4-5": "claude-sonnet-4-5@20250929", + "claude-haiku-4-5": "claude-haiku-4-5@20251001", } return model_map.get(model, model) async def _setup_vertex_credentials(self) -> dict: """Set up Google Cloud Vertex AI credentials from service account.""" - service_account_path = self.context.get_env('GOOGLE_APPLICATION_CREDENTIALS', '').strip() - project_id = self.context.get_env('ANTHROPIC_VERTEX_PROJECT_ID', '').strip() - region = self.context.get_env('CLOUD_ML_REGION', '').strip() + service_account_path = self.context.get_env( + "GOOGLE_APPLICATION_CREDENTIALS", "" + ).strip() + project_id = self.context.get_env("ANTHROPIC_VERTEX_PROJECT_ID", "").strip() + region = self.context.get_env("CLOUD_ML_REGION", "").strip() if not service_account_path: - raise RuntimeError("GOOGLE_APPLICATION_CREDENTIALS must be set when CLAUDE_CODE_USE_VERTEX=1") + raise RuntimeError( + "GOOGLE_APPLICATION_CREDENTIALS must be set when CLAUDE_CODE_USE_VERTEX=1" + ) if not project_id: - raise RuntimeError("ANTHROPIC_VERTEX_PROJECT_ID must be set when CLAUDE_CODE_USE_VERTEX=1") + raise RuntimeError( + "ANTHROPIC_VERTEX_PROJECT_ID must be set when CLAUDE_CODE_USE_VERTEX=1" + ) if not region: - raise RuntimeError("CLOUD_ML_REGION must be set when CLAUDE_CODE_USE_VERTEX=1") + raise RuntimeError( + "CLOUD_ML_REGION must be set when CLAUDE_CODE_USE_VERTEX=1" + ) if not Path(service_account_path).exists(): - raise RuntimeError(f"Service account key file not found at {service_account_path}") + raise RuntimeError( + f"Service account key file not found at {service_account_path}" + ) logger.info(f"Vertex AI configured: project={project_id}, region={region}") return { - 'credentials_path': service_account_path, - 'project_id': project_id, - 'region': region, + "credentials_path": service_account_path, + "project_id": project_id, + "region": region, } async def _prepare_workspace(self) -> AsyncIterator[BaseEvent]: """Validate workspace prepared by init container. - + The init-hydrate container now handles: - Downloading state from S3 (.claude/, artifacts/, file-uploads/) - Cloning repos to /workspace/repos/ - Cloning workflows to /workspace/workflows/ - + Runner just validates and logs what's ready. """ workspace = Path(self.context.workspace_path) logger.info(f"Validating workspace at {workspace}") - + # Check what was hydrated hydrated_paths = [] for path_name in [".claude", "artifacts", "file-uploads"]: @@ -983,14 +1115,13 @@ async def _prepare_workspace(self) -> AsyncIterator[BaseEvent]: file_count = len([f for f in path_dir.rglob("*") if f.is_file()]) if file_count > 0: hydrated_paths.append(f"{path_name} ({file_count} files)") - + if hydrated_paths: logger.info(f"Hydrated from S3: {', '.join(hydrated_paths)}") else: logger.info("No state hydrated (fresh session)") - - # No further preparation needed - init container did the work + # No further preparation needed - init container did the work async def _validate_prerequisites(self): """Validate prerequisite files exist for phase-based slash commands.""" @@ -1001,9 +1132,18 @@ async def _validate_prerequisites(self): prompt_lower = prompt.strip().lower() prerequisites = { - "/speckit.plan": ("spec.md", "Specification file (spec.md) not found. Please run /speckit.specify first."), - "/speckit.tasks": ("plan.md", "Planning file (plan.md) not found. Please run /speckit.plan first."), - "/speckit.implement": ("tasks.md", "Tasks file (tasks.md) not found. Please run /speckit.tasks first.") + "/speckit.plan": ( + "spec.md", + "Specification file (spec.md) not found. Please run /speckit.specify first.", + ), + "/speckit.tasks": ( + "plan.md", + "Planning file (plan.md) not found. Please run /speckit.plan first.", + ), + "/speckit.implement": ( + "tasks.md", + "Tasks file (tasks.md) not found. Please run /speckit.tasks first.", + ), } for cmd, (required_file, error_msg) in prerequisites.items(): @@ -1026,19 +1166,19 @@ async def _validate_prerequisites(self): async def _initialize_workflow_if_set(self) -> AsyncIterator[BaseEvent]: """Validate workflow was cloned by init container.""" - active_workflow_url = (os.getenv('ACTIVE_WORKFLOW_GIT_URL') or '').strip() + active_workflow_url = (os.getenv("ACTIVE_WORKFLOW_GIT_URL") or "").strip() if not active_workflow_url: return try: owner, repo, _ = self._parse_owner_repo(active_workflow_url) - derived_name = repo or '' + derived_name = repo or "" if not derived_name: p = urlparse(active_workflow_url) - parts = [pt for pt in (p.path or '').split('/') if pt] + parts = [pt for pt in (p.path or "").split("/") if pt] if parts: derived_name = parts[-1] - derived_name = (derived_name or '').removesuffix('.git').strip() + derived_name = (derived_name or "").removesuffix(".git").strip() if not derived_name: logger.warning("Could not derive workflow name from URL") @@ -1048,18 +1188,21 @@ async def _initialize_workflow_if_set(self) -> AsyncIterator[BaseEvent]: workspace = Path(self.context.workspace_path) workflow_temp_dir = workspace / "workflows" / f"{derived_name}-clone-temp" workflow_dir = workspace / "workflows" / derived_name - + if workflow_temp_dir.exists(): - logger.info(f"Workflow {derived_name} cloned by init container at {workflow_temp_dir.name}") + logger.info( + f"Workflow {derived_name} cloned by init container at {workflow_temp_dir.name}" + ) elif workflow_dir.exists(): logger.info(f"Workflow {derived_name} available at {workflow_dir.name}") else: - logger.warning(f"Workflow {derived_name} not found (init container may have failed to clone)") + logger.warning( + f"Workflow {derived_name} not found (init container may have failed to clone)" + ) except Exception as e: logger.error(f"Failed to validate workflow: {e}") - async def _run_cmd(self, cmd, cwd=None, capture_stdout=False, ignore_errors=False): """Run a subprocess command asynchronously.""" cmd_safe = [self._redact_secrets(str(arg)) for arg in cmd] @@ -1098,14 +1241,22 @@ def _url_with_token(self, url: str, token: str) -> str: netloc = netloc.split("@", 1)[1] hostname = parsed.hostname or "" - if 'gitlab' in hostname.lower(): + if "gitlab" in hostname.lower(): auth = f"oauth2:{token}@" else: auth = f"x-access-token:{token}@" new_netloc = auth + netloc - return urlunparse((parsed.scheme, new_netloc, parsed.path, - parsed.params, parsed.query, parsed.fragment)) + return urlunparse( + ( + parsed.scheme, + new_netloc, + parsed.path, + parsed.params, + parsed.query, + parsed.fragment, + ) + ) except Exception: return url @@ -1114,17 +1265,19 @@ def _redact_secrets(self, text: str) -> str: if not text: return text - text = re.sub(r'gh[pousr]_[a-zA-Z0-9]{36,255}', 'gh*_***REDACTED***', text) - text = re.sub(r'sk-ant-[a-zA-Z0-9\-_]{30,200}', 'sk-ant-***REDACTED***', text) - text = re.sub(r'pk-lf-[a-zA-Z0-9\-_]{10,100}', 'pk-lf-***REDACTED***', text) - text = re.sub(r'sk-lf-[a-zA-Z0-9\-_]{10,100}', 'sk-lf-***REDACTED***', text) - text = re.sub(r'x-access-token:[^@\s]+@', 'x-access-token:***REDACTED***@', text) - text = re.sub(r'oauth2:[^@\s]+@', 'oauth2:***REDACTED***@', text) - text = re.sub(r'://[^:@\s]+:[^@\s]+@', '://***REDACTED***@', text) + text = re.sub(r"gh[pousr]_[a-zA-Z0-9]{36,255}", "gh*_***REDACTED***", text) + text = re.sub(r"sk-ant-[a-zA-Z0-9\-_]{30,200}", "sk-ant-***REDACTED***", text) + text = re.sub(r"pk-lf-[a-zA-Z0-9\-_]{10,100}", "pk-lf-***REDACTED***", text) + text = re.sub(r"sk-lf-[a-zA-Z0-9\-_]{10,100}", "sk-lf-***REDACTED***", text) + text = re.sub( + r"x-access-token:[^@\s]+@", "x-access-token:***REDACTED***@", text + ) + text = re.sub(r"oauth2:[^@\s]+@", "oauth2:***REDACTED***@", text) + text = re.sub(r"://[^:@\s]+:[^@\s]+@", "://***REDACTED***@", text) text = re.sub( r'(ANTHROPIC_API_KEY|LANGFUSE_SECRET_KEY|LANGFUSE_PUBLIC_KEY|BOT_TOKEN|GIT_TOKEN)\s*=\s*[^\s\'"]+', - r'\1=***REDACTED***', - text + r"\1=***REDACTED***", + text, ) return text @@ -1134,7 +1287,7 @@ async def _fetch_token_for_url(self, url: str) -> str: parsed = urlparse(url) hostname = parsed.hostname or "" - if 'gitlab' in hostname.lower(): + if "gitlab" in hostname.lower(): token = os.getenv("GITLAB_TOKEN", "").strip() if token: logger.info(f"Using GITLAB_TOKEN for {hostname}") @@ -1149,7 +1302,9 @@ async def _fetch_token_for_url(self, url: str) -> str: return token except Exception as e: - logger.warning(f"Failed to parse URL {url}: {e}, falling back to GitHub token") + logger.warning( + f"Failed to parse URL {url}: {e}, falling back to GitHub token" + ) return os.getenv("GITHUB_TOKEN") or await self._fetch_github_token() async def _fetch_github_token(self) -> str: @@ -1160,8 +1315,8 @@ async def _fetch_github_token(self) -> str: return cached # Build mint URL from environment - base = os.getenv('BACKEND_API_URL', '').rstrip('/') - project = os.getenv('PROJECT_NAME', '').strip() + base = os.getenv("BACKEND_API_URL", "").rstrip("/") + project = os.getenv("PROJECT_NAME", "").strip() session_id = self.context.session_id if not base or not project or not session_id: @@ -1171,20 +1326,22 @@ async def _fetch_github_token(self) -> str: url = f"{base}/projects/{project}/agentic-sessions/{session_id}/github/token" logger.info(f"Fetching GitHub token from: {url}") - req = _urllib_request.Request(url, data=b"{}", headers={'Content-Type': 'application/json'}, method='POST') - bot = (os.getenv('BOT_TOKEN') or '').strip() + req = _urllib_request.Request( + url, data=b"{}", headers={"Content-Type": "application/json"}, method="POST" + ) + bot = (os.getenv("BOT_TOKEN") or "").strip() if bot: - req.add_header('Authorization', f'Bearer {bot}') + req.add_header("Authorization", f"Bearer {bot}") loop = asyncio.get_event_loop() def _do_req(): try: with _urllib_request.urlopen(req, timeout=10) as resp: - return resp.read().decode('utf-8', errors='replace') + return resp.read().decode("utf-8", errors="replace") except Exception as e: logger.warning(f"GitHub token fetch failed: {e}") - return '' + return "" resp_text = await loop.run_in_executor(None, _do_req) if not resp_text: @@ -1192,7 +1349,7 @@ def _do_req(): try: data = _json.loads(resp_text) - token = str(data.get('token') or '') + token = str(data.get("token") or "") if token: logger.info("Successfully fetched GitHub token from backend") return token @@ -1236,7 +1393,7 @@ def _get_repos_config(self) -> list[dict]: Returns: [{"name": "repo-name", "url": "...", "branch": "...", "autoPush": bool}, ...] """ try: - raw = os.getenv('REPOS_JSON', '').strip() + raw = os.getenv("REPOS_JSON", "").strip() if not raw: return [] data = _json.loads(raw) @@ -1247,44 +1404,48 @@ def _get_repos_config(self) -> list[dict]: continue # Extract simple format fields - url = str(it.get('url') or '').strip() + url = str(it.get("url") or "").strip() # Auto-generate branch from session name if not provided - branch_from_json = it.get('branch') + branch_from_json = it.get("branch") if branch_from_json and str(branch_from_json).strip(): branch = str(branch_from_json).strip() else: # Fallback: use AGENTIC_SESSION_NAME to match backend logic - session_id = os.getenv('AGENTIC_SESSION_NAME', '').strip() - branch = f"ambient/{session_id}" if session_id else 'main' + session_id = os.getenv("AGENTIC_SESSION_NAME", "").strip() + branch = f"ambient/{session_id}" if session_id else "main" # Parse autoPush as boolean, defaulting to False for invalid types - auto_push_raw = it.get('autoPush', False) - auto_push = auto_push_raw if isinstance(auto_push_raw, bool) else False + auto_push_raw = it.get("autoPush", False) + auto_push = ( + auto_push_raw if isinstance(auto_push_raw, bool) else False + ) if not url: continue # Derive repo name from URL if not provided - name = str(it.get('name') or '').strip() + name = str(it.get("name") or "").strip() if not name: try: owner, repo, _ = self._parse_owner_repo(url) - derived = repo or '' + derived = repo or "" if not derived: p = urlparse(url) - parts = [pt for pt in (p.path or '').split('/') if pt] + parts = [pt for pt in (p.path or "").split("/") if pt] if parts: derived = parts[-1] - name = (derived or '').removesuffix('.git').strip() + name = (derived or "").removesuffix(".git").strip() except Exception: - name = '' + name = "" if name and url: - out.append({ - 'name': name, - 'url': url, - 'branch': branch, - 'autoPush': auto_push - }) + out.append( + { + "name": name, + "url": url, + "branch": branch, + "autoPush": auto_push, + } + ) return out except Exception: return [] @@ -1294,16 +1455,16 @@ def _load_mcp_config(self, cwd_path: str) -> Optional[dict]: """Load MCP server configuration from the ambient runner's .mcp.json file.""" try: # Allow override via MCP_CONFIG_FILE env var (useful for e2e with minimal MCPs) - mcp_config_file = self.context.get_env('MCP_CONFIG_FILE') - if not mcp_config_file or not str(mcp_config_file).strip(): - mcp_config_file = "/app/claude-runner/.mcp.json" + mcp_config_file = self.context.get_env( + "MCP_CONFIG_FILE", "/app/claude-runner/.mcp.json" + ) runner_mcp_file = Path(mcp_config_file) if runner_mcp_file.exists() and runner_mcp_file.is_file(): logger.info(f"Loading MCP config from: {runner_mcp_file}") - with open(runner_mcp_file, 'r') as f: + with open(runner_mcp_file, "r") as f: config = _json.load(f) - return config.get('mcpServers', {}) + return config.get("mcpServers", {}) else: logger.info(f"No MCP config file found at: {runner_mcp_file}") return None @@ -1324,7 +1485,7 @@ def _load_ambient_config(self, cwd_path: str) -> dict: logger.info(f"No ambient.json found at {config_path}, using defaults") return {} - with open(config_path, 'r') as f: + with open(config_path, "r") as f: config = _json.load(f) logger.info(f"Loaded ambient.json: name={config.get('name')}") return config @@ -1336,7 +1497,9 @@ def _load_ambient_config(self, cwd_path: str) -> dict: logger.error(f"Error loading ambient.json: {e}") return {} - def _build_workspace_context_prompt(self, repos_cfg, workflow_name, artifacts_path, ambient_config): + def _build_workspace_context_prompt( + self, repos_cfg, workflow_name, artifacts_path, ambient_config + ): """Generate concise system prompt describing workspace layout.""" prompt = "# Workspace Structure\n\n" @@ -1351,7 +1514,9 @@ def _build_workspace_context_prompt(self, repos_cfg, workflow_name, artifacts_pa file_uploads_path = Path(self.context.workspace_path) / "file-uploads" if file_uploads_path.exists() and file_uploads_path.is_dir(): try: - files = sorted([f.name for f in file_uploads_path.iterdir() if f.is_file()]) + files = sorted( + [f.name for f in file_uploads_path.iterdir() if f.is_file()] + ) if files: max_display = 10 if len(files) <= max_display: @@ -1365,33 +1530,37 @@ def _build_workspace_context_prompt(self, repos_cfg, workflow_name, artifacts_pa # Repositories if repos_cfg: - session_id = os.getenv('AGENTIC_SESSION_NAME', '').strip() + session_id = os.getenv("AGENTIC_SESSION_NAME", "").strip() feature_branch = f"ambient/{session_id}" if session_id else None - - repo_names = [repo.get('name', f'repo-{i}') for i, repo in enumerate(repos_cfg)] + + repo_names = [ + repo.get("name", f"repo-{i}") for i, repo in enumerate(repos_cfg) + ] if len(repo_names) <= 5: prompt += f"**Repositories**: {', '.join([f'repos/{name}/' for name in repo_names])}\n" else: prompt += f"**Repositories** ({len(repo_names)} total): {', '.join([f'repos/{name}/' for name in repo_names[:5]])}, and {len(repo_names) - 5} more\n" - + if feature_branch: prompt += f"**Working Branch**: `{feature_branch}` (all repos are on this feature branch)\n\n" else: prompt += "\n" # Add git push instructions for repos with autoPush enabled - auto_push_repos = [repo for repo in repos_cfg if repo.get('autoPush', False)] + auto_push_repos = [ + repo for repo in repos_cfg if repo.get("autoPush", False) + ] if auto_push_repos: push_branch = feature_branch or "ambient/" - + prompt += "## Git Push Instructions\n\n" prompt += "The following repositories have auto-push enabled. When you make changes to these repositories, you MUST commit and push your changes:\n\n" for repo in auto_push_repos: - repo_name = repo.get('name', 'unknown') + repo_name = repo.get("name", "unknown") prompt += f"- **repos/{repo_name}/**\n" prompt += "\nAfter making changes to any auto-push repository:\n" prompt += "1. Use `git add` to stage your changes\n" - prompt += "2. Use `git commit -m \"description\"` to commit with a descriptive message\n" + prompt += '2. Use `git commit -m "description"` to commit with a descriptive message\n' prompt += f"3. Use `git push origin {push_branch}` to push to the remote repository\n\n" # MCP Integration Setup Instructions @@ -1405,10 +1574,9 @@ def _build_workspace_context_prompt(self, repos_cfg, workflow_name, artifacts_pa return prompt - async def _setup_google_credentials(self): """Copy Google OAuth credentials from mounted Secret to writable workspace location. - + The secret is always mounted (as placeholder if user hasn't authenticated). This method checks if credentials.json exists and has content. Call refresh_google_credentials() periodically to pick up new credentials after OAuth. @@ -1417,21 +1585,26 @@ async def _setup_google_credentials(self): async def _try_copy_google_credentials(self) -> bool: """Attempt to copy Google credentials from mounted secret. - + Returns: True if credentials were successfully copied, False otherwise. """ secret_path = Path("/app/.google_workspace_mcp/credentials/credentials.json") - + # Check if secret file exists if not secret_path.exists(): - logging.debug("Google OAuth credentials not found at %s (placeholder secret or not mounted)", secret_path) + logging.debug( + "Google OAuth credentials not found at %s (placeholder secret or not mounted)", + secret_path, + ) return False - + # Check if file has content (not empty placeholder) try: if secret_path.stat().st_size == 0: - logging.debug("Google OAuth credentials file is empty (user hasn't authenticated yet)") + logging.debug( + "Google OAuth credentials file is empty (user hasn't authenticated yet)" + ) return False except OSError as e: logging.debug("Could not stat Google OAuth credentials file: %s", e) @@ -1447,7 +1620,10 @@ async def _try_copy_google_credentials(self) -> bool: shutil.copy2(secret_path, dest_path) # Make it writable so workspace-mcp can update tokens dest_path.chmod(0o644) - logging.info("✓ Copied Google OAuth credentials from Secret to writable workspace at %s", dest_path) + logging.info( + "✓ Copied Google OAuth credentials from Secret to writable workspace at %s", + dest_path, + ) return True except Exception as e: logging.error("Failed to copy Google OAuth credentials: %s", e) @@ -1455,34 +1631,42 @@ async def _try_copy_google_credentials(self) -> bool: async def refresh_google_credentials(self) -> bool: """Check for and copy new Google OAuth credentials. - + Call this method periodically (e.g., before processing a message) to detect when a user completes the OAuth flow and credentials become available. - + Kubernetes automatically updates the mounted secret volume when the secret changes (typically within ~60 seconds), so this will pick up new credentials without requiring a pod restart. - + Returns: True if new credentials were found and copied, False otherwise. """ - dest_path = Path("/workspace/.google_workspace_mcp/credentials/credentials.json") - + dest_path = Path( + "/workspace/.google_workspace_mcp/credentials/credentials.json" + ) + # If we already have credentials in workspace, check if source is newer if dest_path.exists(): - secret_path = Path("/app/.google_workspace_mcp/credentials/credentials.json") + secret_path = Path( + "/app/.google_workspace_mcp/credentials/credentials.json" + ) if secret_path.exists(): try: # Compare modification times - secret mount updates when K8s syncs if secret_path.stat().st_mtime > dest_path.stat().st_mtime: - logging.info("Detected updated Google OAuth credentials, refreshing...") + logging.info( + "Detected updated Google OAuth credentials, refreshing..." + ) return await self._try_copy_google_credentials() except OSError: pass return False - + # No credentials yet, try to copy if await self._try_copy_google_credentials(): - logging.info("✓ Google OAuth credentials now available (user completed authentication)") + logging.info( + "✓ Google OAuth credentials now available (user completed authentication)" + ) return True - return False \ No newline at end of file + return False diff --git a/components/runners/claude-code-runner/context.py b/components/runners/claude-code-runner/context.py index 4e95abf37..f2a40ffba 100644 --- a/components/runners/claude-code-runner/context.py +++ b/components/runners/claude-code-runner/context.py @@ -3,8 +3,8 @@ """ import os -from typing import Dict, Any, Optional from dataclasses import dataclass, field +from typing import Any, Dict, Optional @dataclass @@ -36,4 +36,3 @@ def set_metadata(self, key: str, value: Any): def get_metadata(self, key: str, default: Any = None) -> Any: """Get metadata value.""" return self.metadata.get(key, default) - diff --git a/components/runners/claude-code-runner/main.py b/components/runners/claude-code-runner/main.py index 31e5bc8c4..c32abe19b 100644 --- a/components/runners/claude-code-runner/main.py +++ b/components/runners/claude-code-runner/main.py @@ -2,20 +2,20 @@ AG-UI Server entry point for Claude Code runner. Implements the official AG-UI server pattern. """ + import asyncio -import os import json import logging +import os from contextlib import asynccontextmanager -from typing import Optional, List, Dict, Any, Union +from typing import Any, Dict, List, Optional, Union -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import StreamingResponse -from pydantic import BaseModel import uvicorn - from ag_ui.core import RunAgentInput from ag_ui.encoder import EventEncoder +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel from context import RunnerContext @@ -26,6 +26,7 @@ # Flexible input model that matches what our frontend actually sends class RunnerInput(BaseModel): """Input model for runner with optional AG-UI fields.""" + threadId: Optional[str] = None thread_id: Optional[str] = None # Support both camelCase and snake_case runId: Optional[str] = None @@ -35,28 +36,30 @@ class RunnerInput(BaseModel): messages: List[Dict[str, Any]] state: Optional[Dict[str, Any]] = None tools: Optional[List[Any]] = None - context: Optional[Union[List[Any], Dict[str, Any]]] = None # Accept both list and dict, convert to list + context: Optional[Union[List[Any], Dict[str, Any]]] = ( + None # Accept both list and dict, convert to list + ) forwardedProps: Optional[Dict[str, Any]] = None environment: Optional[Dict[str, str]] = None metadata: Optional[Dict[str, Any]] = None - + def to_run_agent_input(self) -> RunAgentInput: """Convert to official RunAgentInput model.""" import uuid - + # Normalize field names (prefer camelCase for AG-UI) thread_id = self.threadId or self.thread_id run_id = self.runId or self.run_id parent_run_id = self.parentRunId or self.parent_run_id - + # Generate runId if not provided if not run_id: run_id = str(uuid.uuid4()) logger.info(f"Generated run_id: {run_id}") - + # Context should be a list, not a dict context_list = self.context if isinstance(self.context, list) else [] - + return RunAgentInput( thread_id=thread_id, run_id=run_id, @@ -68,6 +71,7 @@ def to_run_agent_input(self) -> RunAgentInput: forwarded_props=self.forwardedProps or {}, ) + # Global context and adapter context: Optional[RunnerContext] = None adapter = None # Will be ClaudeCodeAdapter after initialization @@ -77,118 +81,134 @@ def to_run_agent_input(self) -> RunAgentInput: async def lifespan(app: FastAPI): """Initialize and cleanup application resources.""" global context, adapter - + # Import adapter here to avoid circular imports from adapter import ClaudeCodeAdapter - + # Initialize context from environment session_id = os.getenv("SESSION_ID", "unknown") workspace_path = os.getenv("WORKSPACE_PATH", "/workspace") - + logger.info(f"Initializing AG-UI server for session {session_id}") - + context = RunnerContext( session_id=session_id, workspace_path=workspace_path, ) - + adapter = ClaudeCodeAdapter() adapter.context = context - + logger.info("Adapter initialized - fresh client will be created for each run") - + # Check if this is a resume session via IS_RESUME env var # This is set by the operator when restarting a stopped/completed/failed session is_resume = os.getenv("IS_RESUME", "").strip().lower() == "true" if is_resume: logger.info("IS_RESUME=true - this is a resumed session") - + # INITIAL_PROMPT is no longer auto-executed on startup # User must explicitly send the first message to start the conversation # Workflow greetings are still triggered when a workflow is activated initial_prompt = os.getenv("INITIAL_PROMPT", "").strip() if initial_prompt: - logger.info(f"INITIAL_PROMPT detected ({len(initial_prompt)} chars) but not auto-executing (user will send first message)") - + logger.info( + f"INITIAL_PROMPT detected ({len(initial_prompt)} chars) but not auto-executing (user will send first message)" + ) + logger.info(f"AG-UI server ready for session {session_id}") - + yield - + # Cleanup logger.info("Shutting down AG-UI server...") async def auto_execute_initial_prompt(prompt: str, session_id: str): """Auto-execute INITIAL_PROMPT by POSTing to backend after short delay. - + The delay gives the runner service time to register in DNS. Backend has retry logic to handle if Service DNS isn't ready yet, so this can be short. - + Only called for fresh sessions (no hydrated state in .claude/). """ import uuid + import aiohttp - + # Configurable delay (default 1s, was 3s) # Backend has retry logic, so we don't need to wait long delay_seconds = float(os.getenv("INITIAL_PROMPT_DELAY_SECONDS", "1")) - logger.info(f"Waiting {delay_seconds}s before auto-executing INITIAL_PROMPT (allow Service DNS to propagate)...") + logger.info( + f"Waiting {delay_seconds}s before auto-executing INITIAL_PROMPT (allow Service DNS to propagate)..." + ) await asyncio.sleep(delay_seconds) - + logger.info("Auto-executing INITIAL_PROMPT via backend POST...") - + # Get backend URL from environment backend_url = os.getenv("BACKEND_API_URL", "").rstrip("/") - project_name = os.getenv("PROJECT_NAME", "").strip() or os.getenv("AGENTIC_SESSION_NAMESPACE", "").strip() - + project_name = ( + os.getenv("PROJECT_NAME", "").strip() + or os.getenv("AGENTIC_SESSION_NAMESPACE", "").strip() + ) + if not backend_url or not project_name: - logger.error("Cannot auto-execute INITIAL_PROMPT: BACKEND_API_URL or PROJECT_NAME not set") + logger.error( + "Cannot auto-execute INITIAL_PROMPT: BACKEND_API_URL or PROJECT_NAME not set" + ) return - + # BACKEND_API_URL already includes /api suffix from operator - url = f"{backend_url}/projects/{project_name}/agentic-sessions/{session_id}/agui/run" + url = ( + f"{backend_url}/projects/{project_name}/agentic-sessions/{session_id}/agui/run" + ) logger.info(f"Auto-execution URL: {url}") - + payload = { "threadId": session_id, "runId": str(uuid.uuid4()), - "messages": [{ - "id": str(uuid.uuid4()), - "role": "user", - "content": prompt, - "metadata": { - "hidden": True, - "autoSent": True, - "source": "runner_initial_prompt" + "messages": [ + { + "id": str(uuid.uuid4()), + "role": "user", + "content": prompt, + "metadata": { + "hidden": True, + "autoSent": True, + "source": "runner_initial_prompt", + }, } - }] + ], } - + # Get BOT_TOKEN for auth bot_token = os.getenv("BOT_TOKEN", "").strip() headers = {"Content-Type": "application/json"} if bot_token: headers["Authorization"] = f"Bearer {bot_token}" - + try: async with aiohttp.ClientSession() as session: - async with session.post(url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp: + async with session.post( + url, + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30), + ) as resp: if resp.status == 200: result = await resp.json() logger.info(f"INITIAL_PROMPT auto-execution started: {result}") else: error_text = await resp.text() - logger.warning(f"INITIAL_PROMPT failed with status {resp.status}: {error_text[:200]}") + logger.warning( + f"INITIAL_PROMPT failed with status {resp.status}: {error_text[:200]}" + ) except Exception as e: logger.warning(f"INITIAL_PROMPT auto-execution error (backend will retry): {e}") - -app = FastAPI( - title="Claude Code AG-UI Server", - version="0.2.0", - lifespan=lifespan -) +app = FastAPI(title="Claude Code AG-UI Server", version="0.2.0", lifespan=lifespan) # Track if adapter has been initialized @@ -201,41 +221,45 @@ async def auto_execute_initial_prompt(prompt: str, session_id: str): async def run_agent(input_data: RunnerInput, request: Request): """ AG-UI compatible run endpoint. - + Accepts flexible input with thread_id, run_id, messages. Optional fields: state, tools, context, forwardedProps. Returns SSE stream of AG-UI events. """ global _adapter_initialized - + if not adapter: raise HTTPException(status_code=503, detail="Adapter not initialized") - + # Convert to official RunAgentInput run_agent_input = input_data.to_run_agent_input() - + # Get Accept header for encoder accept_header = request.headers.get("accept", "text/event-stream") encoder = EventEncoder(accept=accept_header) - - logger.info(f"Processing run: thread_id={run_agent_input.thread_id}, run_id={run_agent_input.run_id}") - + + logger.info( + f"Processing run: thread_id={run_agent_input.thread_id}, run_id={run_agent_input.run_id}" + ) + async def event_generator(): """Generate AG-UI events from adapter.""" global _adapter_initialized - + try: logger.info("Event generator started") - + # Initialize adapter on first run if not _adapter_initialized: - logger.info("First run - initializing adapter with workspace preparation") + logger.info( + "First run - initializing adapter with workspace preparation" + ) await adapter.initialize(context) logger.info("Adapter initialization complete") _adapter_initialized = True - + logger.info("Starting adapter.process_run()...") - + # Process the run (creates fresh client each time) async for event in adapter.process_run(run_agent_input): logger.debug(f"Yielding run event: {event.type}") @@ -244,22 +268,23 @@ async def event_generator(): except Exception as e: logger.error(f"Error in event generator: {e}") # Yield error event - from ag_ui.core import RunErrorEvent, EventType + from ag_ui.core import EventType, RunErrorEvent + error_event = RunErrorEvent( type=EventType.RUN_ERROR, thread_id=run_agent_input.thread_id or context.session_id, run_id=run_agent_input.run_id or "unknown", - message=str(e) + message=str(e), ) yield encoder.encode(error_event) - + return StreamingResponse( event_generator(), media_type=encoder.get_content_type(), headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no", - } + }, ) @@ -267,19 +292,19 @@ async def event_generator(): async def interrupt_run(): """ Interrupt the current Claude SDK execution. - + Sends interrupt signal to Claude subprocess to stop mid-execution. See: https://platform.claude.com/docs/en/agent-sdk/python#methods """ if not adapter: raise HTTPException(status_code=503, detail="Adapter not initialized") - + logger.info("Interrupt request received") - + try: # Call adapter's interrupt method which signals the active Claude SDK client await adapter.interrupt() - + return {"message": "Interrupt signal sent to Claude SDK"} except Exception as e: logger.error(f"Interrupt failed: {e}") @@ -288,6 +313,7 @@ async def interrupt_run(): class FeedbackEvent(BaseModel): """AG-UI META event for user feedback (thumbs up/down).""" + type: str # "META" metaType: str # "thumbs_up" or "thumbs_down" payload: Dict[str, Any] @@ -299,20 +325,24 @@ class FeedbackEvent(BaseModel): async def handle_feedback(event: FeedbackEvent): """ Handle user feedback META events and send to Langfuse. - + This endpoint receives thumbs up/down feedback from the frontend (via backend) and logs it to Langfuse for observability tracking. - + See: https://docs.ag-ui.com/drafts/meta-events#user-feedback """ - logger.info(f"Feedback received: {event.metaType} from {event.payload.get('userId', 'unknown')}") - + logger.info( + f"Feedback received: {event.metaType} from {event.payload.get('userId', 'unknown')}" + ) + if event.type != "META": raise HTTPException(status_code=400, detail="Expected META event type") - + if event.metaType not in ("thumbs_up", "thumbs_down"): - raise HTTPException(status_code=400, detail="metaType must be 'thumbs_up' or 'thumbs_down'") - + raise HTTPException( + status_code=400, detail="metaType must be 'thumbs_up' or 'thumbs_down'" + ) + try: # Extract payload fields payload = event.payload @@ -320,17 +350,19 @@ async def handle_feedback(event: FeedbackEvent): project_name = payload.get("projectName", "") session_name = payload.get("sessionName", "") message_id = payload.get("messageId", "") - trace_id = payload.get("traceId", "") # Langfuse trace ID for specific turn association + trace_id = payload.get( + "traceId", "" + ) # Langfuse trace ID for specific turn association comment = payload.get("comment", "") reason = payload.get("reason", "") workflow = payload.get("workflow", "") context_str = payload.get("context", "") include_transcript = payload.get("includeTranscript", False) transcript = payload.get("transcript", []) - + # Map metaType to boolean value (True = positive, False = negative) value = True if event.metaType == "thumbs_up" else False - + # Build comment string with context comment_parts = [] if comment: @@ -345,27 +377,31 @@ async def handle_feedback(event: FeedbackEvent): for m in transcript ) comment_parts.append(f"\nFull Transcript:\n{transcript_text}") - + feedback_comment = "\n".join(comment_parts) if comment_parts else None - + # Send to Langfuse if configured - langfuse_enabled = os.getenv("LANGFUSE_ENABLED", "").strip().lower() in ("1", "true", "yes") - + langfuse_enabled = os.getenv("LANGFUSE_ENABLED", "").strip().lower() in ( + "1", + "true", + "yes", + ) + if langfuse_enabled: try: from langfuse import Langfuse - + public_key = os.getenv("LANGFUSE_PUBLIC_KEY", "").strip() secret_key = os.getenv("LANGFUSE_SECRET_KEY", "").strip() host = os.getenv("LANGFUSE_HOST", "").strip() - + if public_key and secret_key and host: langfuse = Langfuse( public_key=public_key, secret_key=secret_key, host=host, ) - + # Build metadata for structured filtering in Langfuse UI metadata = { "project": project_name, @@ -377,7 +413,7 @@ async def handle_feedback(event: FeedbackEvent): metadata["workflow"] = workflow if message_id: metadata["messageId"] = message_id - + # Create score directly using create_score() API # Prefer trace_id (specific turn) over session_id (whole session) # Langfuse expects trace_id OR session_id, not both @@ -389,15 +425,19 @@ async def handle_feedback(event: FeedbackEvent): comment=feedback_comment, metadata=metadata, ) - + # Flush immediately to ensure feedback is sent langfuse.flush() - + # Log success after flush completes if trace_id: - logger.info(f"Langfuse: Feedback score sent successfully (trace_id={trace_id}, value={value})") + logger.info( + f"Langfuse: Feedback score sent successfully (trace_id={trace_id}, value={value})" + ) else: - logger.info(f"Langfuse: Feedback score sent successfully (session={session_name}, value={value})") + logger.info( + f"Langfuse: Feedback score sent successfully (session={session_name}, value={value})" + ) else: logger.warning("Langfuse enabled but missing credentials") except ImportError: @@ -405,14 +445,16 @@ async def handle_feedback(event: FeedbackEvent): except Exception as e: logger.error(f"Failed to send feedback to Langfuse: {e}", exc_info=True) else: - logger.info("Langfuse not enabled - feedback logged but not sent to Langfuse") - + logger.info( + "Langfuse not enabled - feedback logged but not sent to Langfuse" + ) + return { "message": "Feedback received", "metaType": event.metaType, "recorded": langfuse_enabled, } - + except Exception as e: logger.error(f"Error processing feedback: {e}") raise HTTPException(status_code=500, detail=str(e)) @@ -421,19 +463,21 @@ async def handle_feedback(event: FeedbackEvent): def _check_mcp_authentication(server_name: str) -> tuple[bool | None, str | None]: """ Check if credentials are available for known MCP servers. - + Returns: Tuple of (is_authenticated, auth_message) Returns (None, None) for servers we don't know how to check """ from pathlib import Path - + # Google Workspace MCP - we know how to check this if server_name == "google-workspace": # Check mounted secret location first, then workspace copy secret_path = Path("/app/.google_workspace_mcp/credentials/credentials.json") - workspace_path = Path("/workspace/.google_workspace_mcp/credentials/credentials.json") - + workspace_path = Path( + "/workspace/.google_workspace_mcp/credentials/credentials.json" + ) + for cred_path in [workspace_path, secret_path]: if cred_path.exists(): try: @@ -442,19 +486,19 @@ def _check_mcp_authentication(server_name: str) -> tuple[bool | None, str | None except OSError: pass return False, "Google OAuth not configured - authenticate via Integrations page" - + # Jira/Atlassian MCP - we know how to check this if server_name in ("mcp-atlassian", "jira"): jira_url = os.getenv("JIRA_URL", "").strip() jira_token = os.getenv("JIRA_API_TOKEN", "").strip() - + if jira_url and jira_token: return True, "Jira credentials configured" elif jira_url: return False, "Jira URL set but API token missing" else: return False, "Jira not configured - set credentials in Workspace Settings" - + # For all other servers (webfetch, unknown) - don't claim to know auth status return None, None @@ -464,127 +508,138 @@ async def get_mcp_status(): """ Returns MCP servers configured for this session with authentication status. Goes straight to the source - uses adapter's _load_mcp_config() method. - + For known integrations (Google, Jira), also checks if credentials are present. """ try: global adapter - + if not adapter: return { "servers": [], "totalCount": 0, - "message": "Adapter not initialized yet" + "message": "Adapter not initialized yet", } - + mcp_servers_list = [] - + # Get the working directory (same logic as adapter uses) - workspace_path = adapter.context.workspace_path if adapter.context else "/workspace" - - active_workflow_url = os.getenv('ACTIVE_WORKFLOW_GIT_URL', '').strip() + workspace_path = ( + adapter.context.workspace_path if adapter.context else "/workspace" + ) + + active_workflow_url = os.getenv("ACTIVE_WORKFLOW_GIT_URL", "").strip() cwd_path = workspace_path - + if active_workflow_url: workflow_name = active_workflow_url.split("/")[-1].removesuffix(".git") workflow_path = os.path.join(workspace_path, "workflows", workflow_name) if os.path.exists(workflow_path): cwd_path = workflow_path - + # Use adapter's method to load MCP config (same as it does during runs) mcp_config = adapter._load_mcp_config(cwd_path) logger.info(f"MCP config: {mcp_config}") - + if mcp_config: for server_name, server_config in mcp_config.items(): # Check authentication status for known servers (Google, Jira) is_authenticated, auth_message = _check_mcp_authentication(server_name) - + # Platform servers are built-in (webfetch), workflow servers come from config is_platform = server_name == "webfetch" - + server_info = { "name": server_name, - "displayName": server_name.replace('-', ' ').replace('_', ' ').title(), + "displayName": server_name.replace("-", " ") + .replace("_", " ") + .title(), "status": "configured", "command": server_config.get("command", ""), - "source": "platform" if is_platform else "workflow" + "source": "platform" if is_platform else "workflow", } - + # Only include auth fields for servers we know how to check if is_authenticated is not None: server_info["authenticated"] = is_authenticated server_info["authMessage"] = auth_message - + mcp_servers_list.append(server_info) - + return { "servers": mcp_servers_list, "totalCount": len(mcp_servers_list), - "note": "Status shows 'configured' - check 'authenticated' field for credential status" + "note": "Status shows 'configured' - check 'authenticated' field for credential status", } - + except Exception as e: logger.error(f"Failed to get MCP status: {e}", exc_info=True) - return { - "servers": [], - "totalCount": 0, - "error": str(e) - } + return {"servers": [], "totalCount": 0, "error": str(e)} -async def clone_workflow_at_runtime(git_url: str, branch: str, subpath: str) -> tuple[bool, str]: +async def clone_workflow_at_runtime( + git_url: str, branch: str, subpath: str +) -> tuple[bool, str]: """ Clone a workflow repository at runtime. - + This mirrors the logic in hydrate.sh but runs when workflows are changed after the pod has started. - + Returns: (success, workflow_dir_path) tuple """ - import tempfile import shutil + import tempfile from pathlib import Path - + if not git_url: return False, "" - + # Derive workflow name from URL workflow_name = git_url.split("/")[-1].removesuffix(".git") workspace_path = os.getenv("WORKSPACE_PATH", "/workspace") workflow_final = Path(workspace_path) / "workflows" / workflow_name - + logger.info(f"Cloning workflow '{workflow_name}' from {git_url}@{branch}") if subpath: logger.info(f" Subpath: {subpath}") - + # Create temp directory for clone temp_dir = Path(tempfile.mkdtemp(prefix="workflow-clone-")) - + try: # Build git clone command with optional auth token github_token = os.getenv("GITHUB_TOKEN", "").strip() gitlab_token = os.getenv("GITLAB_TOKEN", "").strip() - + # Determine which token to use based on URL clone_url = git_url if github_token and "github" in git_url.lower(): - clone_url = git_url.replace("https://", f"https://x-access-token:{github_token}@") + clone_url = git_url.replace( + "https://", f"https://x-access-token:{github_token}@" + ) logger.info("Using GITHUB_TOKEN for workflow authentication") elif gitlab_token and "gitlab" in git_url.lower(): clone_url = git_url.replace("https://", f"https://oauth2:{gitlab_token}@") logger.info("Using GITLAB_TOKEN for workflow authentication") - + # Clone the repository process = await asyncio.create_subprocess_exec( - "git", "clone", "--branch", branch, "--single-branch", "--depth", "1", - clone_url, str(temp_dir), + "git", + "clone", + "--branch", + branch, + "--single-branch", + "--depth", + "1", + clone_url, + str(temp_dir), stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() - + if process.returncode != 0: # Redact tokens from error message error_msg = stderr.decode() @@ -594,9 +649,9 @@ async def clone_workflow_at_runtime(git_url: str, branch: str, subpath: str) -> error_msg = error_msg.replace(gitlab_token, "***REDACTED***") logger.error(f"Failed to clone workflow: {error_msg}") return False, "" - + logger.info("Clone successful, processing...") - + # Handle subpath extraction if subpath: subpath_full = temp_dir / subpath @@ -619,10 +674,10 @@ async def clone_workflow_at_runtime(git_url: str, branch: str, subpath: str) -> if workflow_final.exists(): shutil.rmtree(workflow_final) shutil.move(str(temp_dir), str(workflow_final)) - + logger.info(f"Workflow '{workflow_name}' ready at {workflow_final}") return True, str(workflow_final) - + except Exception as e: logger.error(f"Error cloning workflow: {e}") return False, "" @@ -636,19 +691,19 @@ async def clone_workflow_at_runtime(git_url: str, branch: str, subpath: str) -> async def change_workflow(request: Request): """ Change active workflow - triggers Claude SDK client restart and new greeting. - + Accepts: {"gitUrl": "...", "branch": "...", "path": "..."} """ global _adapter_initialized - + if not adapter: raise HTTPException(status_code=503, detail="Adapter not initialized") - + body = await request.json() git_url = (body.get("gitUrl") or "").strip() branch = (body.get("branch") or "main").strip() or "main" path = (body.get("path") or "").strip() - + logger.info(f"Workflow change request: {git_url}@{branch} (path: {path})") async with _workflow_change_lock: @@ -662,14 +717,23 @@ async def change_workflow(request: Request): and current_path == path ): logger.info("Workflow unchanged; skipping reinit and greeting") - return {"message": "Workflow already active", "gitUrl": git_url, "branch": branch, "path": path} + return { + "message": "Workflow already active", + "gitUrl": git_url, + "branch": branch, + "path": path, + } # Clone the workflow repository at runtime # This is needed because the init container only runs once at pod startup if git_url: - success, workflow_path = await clone_workflow_at_runtime(git_url, branch, path) + success, workflow_path = await clone_workflow_at_runtime( + git_url, branch, path + ) if not success: - logger.warning("Failed to clone workflow, will use default workflow directory") + logger.warning( + "Failed to clone workflow, will use default workflow directory" + ) # Update environment variables os.environ["ACTIVE_WORKFLOW_GIT_URL"] = git_url @@ -686,7 +750,12 @@ async def change_workflow(request: Request): # This runs in background via backend POST asyncio.create_task(trigger_workflow_greeting(git_url, branch, path)) - return {"message": "Workflow updated", "gitUrl": git_url, "branch": branch, "path": path} + return { + "message": "Workflow updated", + "gitUrl": git_url, + "branch": branch, + "path": path, + } async def get_default_branch(repo_path: str) -> str: @@ -706,9 +775,13 @@ async def get_default_branch(repo_path: str) -> str: """ # Method 1: symbolic-ref (fast but may not be set) process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "symbolic-ref", "refs/remotes/origin/HEAD", + "git", + "-C", + str(repo_path), + "symbolic-ref", + "refs/remotes/origin/HEAD", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode == 0: @@ -720,9 +793,14 @@ async def get_default_branch(repo_path: str) -> str: # Method 2: remote show origin (more reliable) process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "remote", "show", "origin", + "git", + "-C", + str(repo_path), + "remote", + "show", + "origin", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode == 0: @@ -737,9 +815,14 @@ async def get_default_branch(repo_path: str) -> str: # Method 3: Try common default branch names for candidate in ["main", "master", "develop"]: process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "rev-parse", "--verify", f"origin/{candidate}", + "git", + "-C", + str(repo_path), + "rev-parse", + "--verify", + f"origin/{candidate}", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) await process.communicate() if process.returncode == 0: @@ -751,7 +834,9 @@ async def get_default_branch(repo_path: str) -> str: return "main" -async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[bool, str, bool]: +async def clone_repo_at_runtime( + git_url: str, branch: str, name: str +) -> tuple[bool, str, bool]: """ Clone a repository at runtime or add a new branch to existing repo. @@ -773,8 +858,8 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b - repo_dir_path: Path to the repo directory - was_newly_cloned: True only if the repo was actually cloned this time """ - import tempfile import shutil + import tempfile from pathlib import Path if not git_url: @@ -787,7 +872,9 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Generate unique branch name if not specified (only if user didn't provide one) # IMPORTANT: Keep in sync with backend (sessions.go) and frontend (add-context-modal.tsx) if not branch or branch.strip() == "": - session_id = os.getenv("AGENTIC_SESSION_NAME", "").strip() or os.getenv("SESSION_ID", "unknown") + session_id = os.getenv("AGENTIC_SESSION_NAME", "").strip() or os.getenv( + "SESSION_ID", "unknown" + ) branch = f"ambient/{session_id}" logger.info(f"No branch specified, auto-generated: {branch}") @@ -802,7 +889,9 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b gitlab_token = os.getenv("GITLAB_TOKEN", "").strip() clone_url = git_url if github_token and "github" in git_url.lower(): - clone_url = git_url.replace("https://", f"https://x-access-token:{github_token}@") + clone_url = git_url.replace( + "https://", f"https://x-access-token:{github_token}@" + ) logger.info("Using GITHUB_TOKEN for authentication") elif gitlab_token and "gitlab" in git_url.lower(): clone_url = git_url.replace("https://", f"https://oauth2:{gitlab_token}@") @@ -810,21 +899,31 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Case 1: Repo already exists - add new branch if repo_final.exists(): - logger.info(f"Repo '{name}' already exists at {repo_final}, adding branch '{branch}'") + logger.info( + f"Repo '{name}' already exists at {repo_final}, adding branch '{branch}'" + ) try: # Fetch latest refs process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_final), "fetch", "origin", + "git", + "-C", + str(repo_final), + "fetch", + "origin", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) await process.communicate() # Try to checkout the branch process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_final), "checkout", branch, + "git", + "-C", + str(repo_final), + "checkout", + branch, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -835,9 +934,15 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Branch doesn't exist locally, try to checkout from remote logger.info(f"Branch '{branch}' not found locally, trying origin/{branch}") process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_final), "checkout", "-b", branch, f"origin/{branch}", + "git", + "-C", + str(repo_final), + "checkout", + "-b", + branch, + f"origin/{branch}", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -846,24 +951,35 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b return True, str(repo_final), False # Already existed, not newly cloned # Branch doesn't exist remotely, create from default branch - logger.info(f"Branch '{branch}' not found on remote, creating from default branch") + logger.info( + f"Branch '{branch}' not found on remote, creating from default branch" + ) # Get default branch using robust detection default_branch = await get_default_branch(str(repo_final)) # Checkout default branch first process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_final), "checkout", default_branch, + "git", + "-C", + str(repo_final), + "checkout", + default_branch, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) await process.communicate() # Create new branch from default process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_final), "checkout", "-b", branch, + "git", + "-C", + str(repo_final), + "checkout", + "-b", + branch, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -888,10 +1004,12 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Clone without --single-branch to support multi-branch workflows # No --depth=1 to allow full branch operations process = await asyncio.create_subprocess_exec( - "git", "clone", - clone_url, str(temp_dir), + "git", + "clone", + clone_url, + str(temp_dir), stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -908,9 +1026,13 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Try to checkout requested/auto-generated branch process = await asyncio.create_subprocess_exec( - "git", "-C", str(temp_dir), "checkout", branch, + "git", + "-C", + str(temp_dir), + "checkout", + branch, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -918,13 +1040,17 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b # Branch doesn't exist, create it from default branch logger.info(f"Branch '{branch}' not found, creating from default branch") process = await asyncio.create_subprocess_exec( - "git", "-C", str(temp_dir), "checkout", "-b", branch, + "git", + "-C", + str(temp_dir), + "checkout", + "-b", + branch, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) await process.communicate() - # Move to final location logger.info("Moving to final location...") repo_final.parent.mkdir(parents=True, exist_ok=True) @@ -944,51 +1070,56 @@ async def clone_repo_at_runtime(git_url: str, branch: str, name: str) -> tuple[b async def trigger_workflow_greeting(git_url: str, branch: str, path: str): """Trigger workflow greeting after workflow change.""" import uuid + import aiohttp - + # Wait a moment for workflow to be cloned/initialized await asyncio.sleep(3) - + logger.info("Triggering workflow greeting...") - + try: backend_url = os.getenv("BACKEND_API_URL", "").rstrip("/") project_name = os.getenv("AGENTIC_SESSION_NAMESPACE", "").strip() session_id = context.session_id if context else "unknown" - + if not backend_url or not project_name: - logger.error("Cannot trigger workflow greeting: BACKEND_API_URL or PROJECT_NAME not set") + logger.error( + "Cannot trigger workflow greeting: BACKEND_API_URL or PROJECT_NAME not set" + ) return - + url = f"{backend_url}/projects/{project_name}/agentic-sessions/{session_id}/agui/run" - + # Extract workflow name for greeting workflow_name = git_url.split("/")[-1].removesuffix(".git") if path: workflow_name = path.split("/")[-1] - + greeting = f"Greet the user and explain that the {workflow_name} workflow is now active. Briefly describe what this workflow helps with based on the systemPrompt in ambient.json. Keep it concise and friendly." - + payload = { "threadId": session_id, "runId": str(uuid.uuid4()), - "messages": [{ - "id": str(uuid.uuid4()), - "role": "user", - "content": greeting, - "metadata": { - "hidden": True, - "autoSent": True, - "source": "workflow_activation" + "messages": [ + { + "id": str(uuid.uuid4()), + "role": "user", + "content": greeting, + "metadata": { + "hidden": True, + "autoSent": True, + "source": "workflow_activation", + }, } - }] + ], } - + bot_token = os.getenv("BOT_TOKEN", "").strip() headers = {"Content-Type": "application/json"} if bot_token: headers["Authorization"] = f"Bearer {bot_token}" - + async with aiohttp.ClientSession() as session: async with session.post(url, json=payload, headers=headers) as resp: if resp.status == 200: @@ -996,8 +1127,10 @@ async def trigger_workflow_greeting(git_url: str, branch: str, path: str): logger.info(f"Workflow greeting started: {result}") else: error_text = await resp.text() - logger.error(f"Workflow greeting failed: {resp.status} - {error_text}") - + logger.error( + f"Workflow greeting failed: {resp.status} - {error_text}" + ) + except Exception as e: logger.error(f"Failed to trigger workflow greeting: {e}") @@ -1006,32 +1139,36 @@ async def trigger_workflow_greeting(git_url: str, branch: str, path: str): async def add_repo(request: Request): """ Add repository - clones repo and triggers Claude SDK client restart. - + Accepts: {"url": "...", "branch": "...", "name": "..."} """ global _adapter_initialized - + if not adapter: raise HTTPException(status_code=503, detail="Adapter not initialized") - + body = await request.json() url = body.get("url", "") branch = body.get("branch", "main") name = body.get("name", "") - + logger.info(f"Add repo request: url={url}, branch={branch}, name={name}") - + if not url: raise HTTPException(status_code=400, detail="Repository URL is required") - + # Derive name from URL if not provided if not name: name = url.split("/")[-1].removesuffix(".git") - + # Clone the repository at runtime - success, repo_path, was_newly_cloned = await clone_repo_at_runtime(url, branch, name) + success, repo_path, was_newly_cloned = await clone_repo_at_runtime( + url, branch, name + ) if not success: - raise HTTPException(status_code=500, detail=f"Failed to clone repository: {url}") + raise HTTPException( + status_code=500, detail=f"Failed to clone repository: {url}" + ) # Only update state and trigger notification if repo was newly cloned # This prevents duplicate notifications when both backend and operator call this endpoint @@ -1044,13 +1181,7 @@ async def add_repo(request: Request): repos = [] # Add new repo - repos.append({ - "name": name, - "input": { - "url": url, - "branch": branch - } - }) + repos.append({"name": name, "input": {"url": url, "branch": branch}}) os.environ["REPOS_JSON"] = json.dumps(repos) @@ -1058,59 +1189,73 @@ async def add_repo(request: Request): _adapter_initialized = False adapter._first_run = True - logger.info(f"Repo '{name}' added and cloned, adapter will reinitialize on next run") + logger.info( + f"Repo '{name}' added and cloned, adapter will reinitialize on next run" + ) # Trigger a notification to Claude about the new repository asyncio.create_task(trigger_repo_added_notification(name, url)) else: - logger.info(f"Repo '{name}' already existed, skipping notification (idempotent call)") + logger.info( + f"Repo '{name}' already existed, skipping notification (idempotent call)" + ) - return {"message": "Repository added", "name": name, "path": repo_path, "newly_cloned": was_newly_cloned} + return { + "message": "Repository added", + "name": name, + "path": repo_path, + "newly_cloned": was_newly_cloned, + } async def trigger_repo_added_notification(repo_name: str, repo_url: str): """Notify Claude that a repository has been added.""" import uuid + import aiohttp - + # Wait a moment for repo to be fully ready await asyncio.sleep(1) - + logger.info(f"Triggering repo added notification for: {repo_name}") - + try: backend_url = os.getenv("BACKEND_API_URL", "").rstrip("/") project_name = os.getenv("AGENTIC_SESSION_NAMESPACE", "").strip() session_id = context.session_id if context else "unknown" - + if not backend_url or not project_name: - logger.error("Cannot trigger repo notification: BACKEND_API_URL or PROJECT_NAME not set") + logger.error( + "Cannot trigger repo notification: BACKEND_API_URL or PROJECT_NAME not set" + ) return - + url = f"{backend_url}/projects/{project_name}/agentic-sessions/{session_id}/agui/run" - + notification = f"The repository '{repo_name}' has been added to your workspace. You can now access it at the path 'repos/{repo_name}/'. Please acknowledge this to the user and let them know you can now read and work with files in this repository." - + payload = { "threadId": session_id, "runId": str(uuid.uuid4()), - "messages": [{ - "id": str(uuid.uuid4()), - "role": "user", - "content": notification, - "metadata": { - "hidden": True, - "autoSent": True, - "source": "repo_added" + "messages": [ + { + "id": str(uuid.uuid4()), + "role": "user", + "content": notification, + "metadata": { + "hidden": True, + "autoSent": True, + "source": "repo_added", + }, } - }] + ], } - + bot_token = os.getenv("BOT_TOKEN", "").strip() headers = {"Content-Type": "application/json"} if bot_token: headers["Authorization"] = f"Bearer {bot_token}" - + async with aiohttp.ClientSession() as session: async with session.post(url, json=payload, headers=headers) as resp: if resp.status == 200: @@ -1118,8 +1263,10 @@ async def trigger_repo_added_notification(repo_name: str, repo_url: str): logger.info(f"Repo notification sent: {result}") else: error_text = await resp.text() - logger.error(f"Repo notification failed: {resp.status} - {error_text}") - + logger.error( + f"Repo notification failed: {resp.status} - {error_text}" + ) + except Exception as e: logger.error(f"Failed to trigger repo notification: {e}") @@ -1153,7 +1300,9 @@ async def remove_repo(request: Request): logger.info(f"Deleted repository directory: {repo_path}") except Exception as e: logger.error(f"Failed to delete repository directory {repo_path}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to delete repository: {e}") + raise HTTPException( + status_code=500, detail=f"Failed to delete repository: {e}" + ) else: logger.warning(f"Repository directory not found: {repo_path}") @@ -1214,45 +1363,67 @@ async def get_repos_status(): # Get remote URL process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "config", "--get", "remote.origin.url", + "git", + "-C", + str(repo_path), + "config", + "--get", + "remote.origin.url", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() repo_url = stdout.decode().strip() if process.returncode == 0 else "" # Strip any embedded tokens from URL before returning (security) # Remove patterns like: https://x-access-token:TOKEN@github.com -> https://github.com - repo_url = re.sub(r'https://[^:]+:[^@]+@', 'https://', repo_url) + repo_url = re.sub(r"https://[^:]+:[^@]+@", "https://", repo_url) # Get current active branch process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "rev-parse", "--abbrev-ref", "HEAD", + "git", + "-C", + str(repo_path), + "rev-parse", + "--abbrev-ref", + "HEAD", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() - current_branch = stdout.decode().strip() if process.returncode == 0 else "unknown" + current_branch = ( + stdout.decode().strip() if process.returncode == 0 else "unknown" + ) # Get all local branches process = await asyncio.create_subprocess_exec( - "git", "-C", str(repo_path), "branch", "--format=%(refname:short)", + "git", + "-C", + str(repo_path), + "branch", + "--format=%(refname:short)", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() - branches = [b.strip() for b in stdout.decode().split("\n") if b.strip()] if process.returncode == 0 else [] + branches = ( + [b.strip() for b in stdout.decode().split("\n") if b.strip()] + if process.returncode == 0 + else [] + ) # Get default branch using robust detection default_branch = await get_default_branch(str(repo_path)) - repos_status.append({ - "url": repo_url, - "name": repo_name, - "branches": branches, - "currentActiveBranch": current_branch, - "defaultBranch": default_branch, - }) + repos_status.append( + { + "url": repo_url, + "name": repo_name, + "branches": branches, + "currentActiveBranch": current_branch, + "defaultBranch": default_branch, + } + ) except Exception as e: logger.error(f"Error getting status for repo {repo_path}: {e}") @@ -1274,9 +1445,9 @@ def main(): """Start the AG-UI server.""" port = int(os.getenv("AGUI_PORT", "8000")) host = os.getenv("AGUI_HOST", "0.0.0.0") - + logger.info(f"Starting Claude Code AG-UI server on {host}:{port}") - + uvicorn.run( app, host=host, @@ -1287,4 +1458,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/components/runners/claude-code-runner/observability.py b/components/runners/claude-code-runner/observability.py index b8be5bac1..a06a296c7 100644 --- a/components/runners/claude-code-runner/observability.py +++ b/components/runners/claude-code-runner/observability.py @@ -43,8 +43,8 @@ Reference: https://langfuse.com/docs/observability/sdk/python/sdk-v3 """ -import os import logging +import os from typing import Any from urllib.parse import urlparse @@ -86,11 +86,27 @@ def _privacy_masking_function(data: Any, **kwargs) -> Any: masked = {} for key, value in data.items(): # Preserve usage and metadata fields - these don't contain sensitive data - if key in ("usage", "usage_details", "metadata", "model", "turn", - "input_tokens", "output_tokens", "cache_read_input_tokens", - "cache_creation_input_tokens", "total_tokens", "cost_usd", - "duration_ms", "duration_api_ms", "num_turns", "session_id", - "tool_id", "tool_name", "is_error", "level"): + if key in ( + "usage", + "usage_details", + "metadata", + "model", + "turn", + "input_tokens", + "output_tokens", + "cache_read_input_tokens", + "cache_creation_input_tokens", + "total_tokens", + "cost_usd", + "duration_ms", + "duration_api_ms", + "num_turns", + "session_id", + "tool_id", + "tool_name", + "is_error", + "level", + ): masked[key] = value # Redact content fields that may contain user data elif key in ("content", "text", "input", "output", "prompt", "completion"): @@ -112,8 +128,7 @@ def _privacy_masking_function(data: Any, **kwargs) -> Any: class ObservabilityManager: - """Manages Langfuse observability for Claude sessions. - """ + """Manages Langfuse observability for Claude sessions.""" def __init__(self, session_id: str, user_id: str, user_name: str): """Initialize observability manager. @@ -129,7 +144,9 @@ def __init__(self, session_id: str, user_id: str, user_name: str): self.langfuse_client = None self._propagate_ctx = None self._tool_spans: dict[str, Any] = {} # Stores span objects directly - self._current_turn_generation = None # Track active turn for tool span parenting + self._current_turn_generation = ( + None # Track active turn for tool span parenting + ) self._current_turn_ctx = None # Track turn context manager for proper cleanup self._pending_initial_prompt = None # Store initial prompt for turn 1 @@ -170,13 +187,19 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo return False if not host: - logging.warning("LANGFUSE_HOST is missing. Add to secret (e.g., http://langfuse:3000).") + logging.warning( + "LANGFUSE_HOST is missing. Add to secret (e.g., http://langfuse:3000)." + ) return False # Validate host format try: parsed = urlparse(host) - if not parsed.scheme or not parsed.netloc or parsed.scheme not in ("http", "https"): + if ( + not parsed.scheme + or not parsed.netloc + or parsed.scheme not in ("http", "https") + ): logging.warning(f"LANGFUSE_HOST invalid format: {host}") return False except Exception as e: @@ -187,29 +210,32 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo # Determine if message masking should be enabled # Default: MASK messages (privacy-first approach) # Set LANGFUSE_MASK_MESSAGES=false to explicitly disable masking (dev/testing only) - mask_messages_env = os.getenv("LANGFUSE_MASK_MESSAGES", "true").strip().lower() + mask_messages_env = ( + os.getenv("LANGFUSE_MASK_MESSAGES", "true").strip().lower() + ) enable_masking = mask_messages_env not in ("false", "0", "no") if enable_masking: - logging.info("Langfuse: Privacy masking ENABLED - user messages and responses will be redacted") + logging.info( + "Langfuse: Privacy masking ENABLED - user messages and responses will be redacted" + ) mask_fn = _privacy_masking_function else: - logging.warning("Langfuse: Privacy masking DISABLED - full message content will be logged (use only for dev/testing)") + logging.warning( + "Langfuse: Privacy masking DISABLED - full message content will be logged (use only for dev/testing)" + ) mask_fn = None # Initialize client with optional masking self.langfuse_client = Langfuse( - public_key=public_key, - secret_key=secret_key, - host=host, - mask=mask_fn + public_key=public_key, secret_key=secret_key, host=host, mask=mask_fn ) # Build metadata with model information metadata = { "namespace": namespace, "user_name": self.user_name, - "initial_prompt": prompt[:200] if len(prompt) > 200 else prompt + "initial_prompt": prompt[:200] if len(prompt) > 200 else prompt, } # Build tags list @@ -225,9 +251,13 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo metadata["model"] = sanitized_model # Add model as a tag for easy filtering in Langfuse UI tags.append(f"model:{sanitized_model}") - logging.info(f"Langfuse: Model '{sanitized_model}' added to session metadata and tags") + logging.info( + f"Langfuse: Model '{sanitized_model}' added to session metadata and tags" + ) else: - logging.warning(f"Langfuse: Model name '{model}' failed sanitization - omitting from metadata") + logging.warning( + f"Langfuse: Model name '{model}' failed sanitization - omitting from metadata" + ) # Enter propagate_attributes context - all traces share session_id/user_id/tags/metadata # Each turn will be a separate trace, automatically grouped by session_id @@ -237,7 +267,7 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo user_id=self.user_id, session_id=self.session_id, tags=tags, - metadata=metadata + metadata=metadata, ) self._propagate_ctx.__enter__() except Exception: @@ -250,7 +280,9 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo self._propagate_ctx = None raise - logging.info(f"Langfuse: Session tracking enabled (session_id={self.session_id}, user_id={self.user_id}, model={model})") + logging.info( + f"Langfuse: Session tracking enabled (session_id={self.session_id}, user_id={self.user_id}, model={model})" + ) return True except Exception as e: @@ -293,7 +325,9 @@ def start_turn(self, model: str, user_input: str | None = None) -> None: # Guard: Prevent creating duplicate traces for the same turn # SDK sends multiple AssistantMessages during streaming - only create trace once if self._current_turn_generation: - logging.debug("Langfuse: Trace already active for current turn, skipping duplicate start_turn") + logging.debug( + "Langfuse: Trace already active for current turn, skipping duplicate start_turn" + ) return try: @@ -306,7 +340,9 @@ def start_turn(self, model: str, user_input: str | None = None) -> None: # Use actual user input if provided, otherwise use generic placeholder if user_input: input_content = [{"role": "user", "content": user_input}] - logging.info(f"Langfuse: Starting turn trace with model={model} and actual user input") + logging.info( + f"Langfuse: Starting turn trace with model={model} and actual user input" + ) else: input_content = [{"role": "user", "content": "User input"}] logging.info(f"Langfuse: Starting turn trace with model={model}") @@ -323,7 +359,9 @@ def start_turn(self, model: str, user_input: str | None = None) -> None: metadata={}, # Turn number will be added in end_turn() ) self._current_turn_generation = self._current_turn_ctx.__enter__() - logging.info(f"Langfuse: Created new trace (model={model}, trace_id={self.get_current_trace_id()})") + logging.info( + f"Langfuse: Created new trace (model={model}, trace_id={self.get_current_trace_id()})" + ) except Exception as e: logging.error(f"Langfuse: Failed to start turn: {e}", exc_info=True) @@ -336,14 +374,16 @@ def get_current_trace_id(self) -> str | None: """ if not self._current_turn_generation: return None - + # The generation object has a trace_id attribute try: - return getattr(self._current_turn_generation, 'trace_id', None) + return getattr(self._current_turn_generation, "trace_id", None) except Exception: return None - def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> None: + def end_turn( + self, turn_count: int, message: Any, usage: dict | None = None + ) -> None: """Complete turn tracking with output and usage data (called when ResultMessage arrives). Updates the turn generation with the assistant's output, usage metrics, and SDK's @@ -357,9 +397,11 @@ def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> # Return silently if Langfuse not initialized if not self.langfuse_client: return - + if not self._current_turn_generation: - logging.debug(f"Langfuse: end_turn called but no active turn for turn {turn_count} (may not be initialized)") + logging.debug( + f"Langfuse: end_turn called but no active turn for turn {turn_count} (may not be initialized)" + ) return try: @@ -372,7 +414,9 @@ def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> if isinstance(blk, TextBlock): text_content.append(getattr(blk, "text", "")) - output_text = "\n".join(text_content) if text_content else "(no text output)" + output_text = ( + "\n".join(text_content) if text_content else "(no text output)" + ) # Calculate usage_details if we have usage data usage_details_dict = None @@ -402,7 +446,7 @@ def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> # SDK v3 requires 'usage_details' parameter for usage tracking update_params = { "output": output_text, - "metadata": {"turn": turn_count} # Add SDK's authoritative turn number + "metadata": {"turn": turn_count}, # Add SDK's authoritative turn number } if usage_details_dict: update_params["usage_details"] = usage_details_dict @@ -423,14 +467,20 @@ def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> self.langfuse_client.flush() logging.info(f"Langfuse: Flushed turn {turn_count} data") except Exception as e: - logging.warning(f"Langfuse: Flush failed after turn {turn_count}: {e}") + logging.warning( + f"Langfuse: Flush failed after turn {turn_count}: {e}" + ) if usage_details_dict: - input_count = usage_details_dict.get('input', 0) - output_count = usage_details_dict.get('output', 0) - cache_read_count = usage_details_dict.get('cache_read_input_tokens', 0) - cache_creation_count = usage_details_dict.get('cache_creation_input_tokens', 0) - total_tokens = input_count + output_count + cache_read_count + cache_creation_count + input_count = usage_details_dict.get("input", 0) + output_count = usage_details_dict.get("output", 0) + cache_read_count = usage_details_dict.get("cache_read_input_tokens", 0) + cache_creation_count = usage_details_dict.get( + "cache_creation_input_tokens", 0 + ) + total_tokens = ( + input_count + output_count + cache_read_count + cache_creation_count + ) log_msg = ( f"Langfuse: Completed turn {turn_count} - " @@ -450,7 +500,9 @@ def end_turn(self, turn_count: int, message: Any, usage: dict | None = None) -> try: self._current_turn_ctx.__exit__(None, None, None) except Exception as cleanup_error: - logging.warning(f"Langfuse: Cleanup during error failed: {cleanup_error}") + logging.warning( + f"Langfuse: Cleanup during error failed: {cleanup_error}" + ) self._current_turn_generation = None self._current_turn_ctx = None @@ -479,21 +531,27 @@ def track_tool_use(self, tool_name: str, tool_id: str, tool_input: dict) -> None as_type="span", name=f"tool_{tool_name}", input=tool_input, - metadata={"tool_id": tool_id, "tool_name": tool_name} + metadata={"tool_id": tool_id, "tool_name": tool_name}, ) self._tool_spans[tool_id] = span - logging.debug(f"Langfuse: Started tool span for {tool_name} (id={tool_id}) under turn") + logging.debug( + f"Langfuse: Started tool span for {tool_name} (id={tool_id}) under turn" + ) else: # Fallback: create orphaned span if no active turn (shouldn't happen) - logging.warning(f"No active turn for tool {tool_name}, creating orphaned span") + logging.warning( + f"No active turn for tool {tool_name}, creating orphaned span" + ) span = self.langfuse_client.start_observation( as_type="span", name=f"tool_{tool_name}", input=tool_input, - metadata={"tool_id": tool_id, "tool_name": tool_name} + metadata={"tool_id": tool_id, "tool_name": tool_name}, ) self._tool_spans[tool_id] = span - logging.debug(f"Langfuse: Started orphaned tool span for {tool_name} (id={tool_id})") + logging.debug( + f"Langfuse: Started orphaned tool span for {tool_name} (id={tool_id})" + ) except Exception as e: logging.debug(f"Langfuse: Failed to track tool use: {e}") @@ -522,7 +580,7 @@ def track_tool_result(self, tool_use_id: str, content: Any, is_error: bool) -> N tool_span.update( output={"result": result_text}, level="ERROR" if is_error else "DEFAULT", - metadata={"is_error": is_error or False} + metadata={"is_error": is_error or False}, ) # End the span to close it properly @@ -612,9 +670,13 @@ async def cleanup_on_error(self, error: Exception) -> None: try: tool_span.update(level="ERROR") tool_span.end() - logging.debug(f"Langfuse: Closed tool span {tool_id} during error cleanup") + logging.debug( + f"Langfuse: Closed tool span {tool_id} during error cleanup" + ) except Exception as e: - logging.warning(f"Failed to close tool span {tool_id} during error: {e}") + logging.warning( + f"Failed to close tool span {tool_id} during error: {e}" + ) self._tool_spans.clear() # Close propagate context diff --git a/components/runners/claude-code-runner/pyproject.toml b/components/runners/claude-code-runner/pyproject.toml index 5faafe4ac..a36cafa92 100644 --- a/components/runners/claude-code-runner/pyproject.toml +++ b/components/runners/claude-code-runner/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ # Claude SDK "anthropic[vertex]>=0.68.0", - "claude-agent-sdk>=0.1.12", + "claude-agent-sdk>=0.1.23", # Observability "langfuse>=3.0.0", diff --git a/components/runners/claude-code-runner/security_utils.py b/components/runners/claude-code-runner/security_utils.py index ec9e95f5f..e7d46be97 100644 --- a/components/runners/claude-code-runner/security_utils.py +++ b/components/runners/claude-code-runner/security_utils.py @@ -5,10 +5,10 @@ to prevent API key leaks and hanging operations. """ -import re import asyncio import logging -from typing import Callable, Any, TypeVar, ParamSpec +import re +from typing import Any, Callable, ParamSpec, TypeVar P = ParamSpec("P") T = TypeVar("T") @@ -199,7 +199,7 @@ def sanitize_model_name(model: str, max_length: int = 100) -> str | None: return None # Remove any characters that aren't alphanumeric or allowed separators - sanitized = re.sub(r'[^a-zA-Z0-9@.:/_-]', '', model[:max_length]) + sanitized = re.sub(r"[^a-zA-Z0-9@.:/_-]", "", model[:max_length]) # Return None if empty after sanitization return sanitized if sanitized else None diff --git a/components/runners/claude-code-runner/tests/test_auto_push.py b/components/runners/claude-code-runner/tests/test_auto_push.py index df67d2156..1fd600ad6 100644 --- a/components/runners/claude-code-runner/tests/test_auto_push.py +++ b/components/runners/claude-code-runner/tests/test_auto_push.py @@ -1,15 +1,16 @@ """Unit tests for autoPush functionality in adapter.py.""" -import pytest import json import os import sys -from unittest.mock import MagicMock, patch, Mock +from unittest.mock import MagicMock, Mock, patch + +import pytest # Mock ag_ui module before importing adapter -sys.modules['ag_ui'] = Mock() -sys.modules['ag_ui.core'] = Mock() -sys.modules['context'] = Mock() +sys.modules["ag_ui"] = Mock() +sys.modules["ag_ui.core"] = Mock() +sys.modules["context"] = Mock() class TestGetReposConfig: @@ -19,17 +20,20 @@ def setup_method(self): """Set up test fixtures.""" # Import here after mocking dependencies from adapter import ClaudeCodeAdapter + self.adapter = ClaudeCodeAdapter() def test_parse_simple_repo_with_autopush_true(self): """Test parsing repo with autoPush=true.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "branch": "main", - "autoPush": True - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo.git", + "branch": "main", + "autoPush": True, + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -42,13 +46,15 @@ def test_parse_simple_repo_with_autopush_true(self): def test_parse_simple_repo_with_autopush_false(self): """Test parsing repo with autoPush=false.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "branch": "develop", - "autoPush": False - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo.git", + "branch": "develop", + "autoPush": False, + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -58,12 +64,9 @@ def test_parse_simple_repo_with_autopush_false(self): def test_parse_repo_without_autopush(self): """Test parsing repo without autoPush field defaults to False.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "branch": "main" - } - ]) + repos_json = json.dumps( + [{"url": "https://github.com/owner/repo.git", "branch": "main"}] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -73,23 +76,25 @@ def test_parse_repo_without_autopush(self): def test_parse_multiple_repos_mixed_autopush(self): """Test parsing multiple repos with mixed autoPush settings.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo1.git", - "branch": "main", - "autoPush": True - }, - { - "url": "https://github.com/owner/repo2.git", - "branch": "develop", - "autoPush": False - }, - { - "url": "https://github.com/owner/repo3.git", - "branch": "feature" - # No autoPush field - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo1.git", + "branch": "main", + "autoPush": True, + }, + { + "url": "https://github.com/owner/repo2.git", + "branch": "develop", + "autoPush": False, + }, + { + "url": "https://github.com/owner/repo3.git", + "branch": "feature", + # No autoPush field + }, + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -101,14 +106,16 @@ def test_parse_multiple_repos_mixed_autopush(self): def test_parse_repo_with_explicit_name(self): """Test parsing repo with explicit name field.""" - repos_json = json.dumps([ - { - "name": "my-custom-repo", - "url": "https://github.com/owner/repo.git", - "branch": "main", - "autoPush": True - } - ]) + repos_json = json.dumps( + [ + { + "name": "my-custom-repo", + "url": "https://github.com/owner/repo.git", + "branch": "main", + "autoPush": True, + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -149,12 +156,7 @@ def test_parse_non_list_json(self): def test_parse_repo_without_url(self): """Test that repos without URL are skipped.""" - repos_json = json.dumps([ - { - "branch": "main", - "autoPush": True - } - ]) + repos_json = json.dumps([{"branch": "main", "autoPush": True}]) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -180,12 +182,14 @@ def test_derive_repo_name_from_url(self): def test_autopush_with_invalid_string_type(self): """Test that string autoPush values default to False.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "autoPush": "true" # String instead of boolean - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo.git", + "autoPush": "true", # String instead of boolean + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -196,12 +200,14 @@ def test_autopush_with_invalid_string_type(self): def test_autopush_with_invalid_number_type(self): """Test that numeric autoPush values default to False.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "autoPush": 1 # Number instead of boolean - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo.git", + "autoPush": 1, # Number instead of boolean + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -212,12 +218,14 @@ def test_autopush_with_invalid_number_type(self): def test_autopush_with_null_value(self): """Test that null autoPush values default to False.""" - repos_json = json.dumps([ - { - "url": "https://github.com/owner/repo.git", - "autoPush": None # null in JSON - } - ]) + repos_json = json.dumps( + [ + { + "url": "https://github.com/owner/repo.git", + "autoPush": None, # null in JSON + } + ] + ) with patch.dict(os.environ, {"REPOS_JSON": repos_json}): result = self.adapter._get_repos_config() @@ -249,7 +257,7 @@ def test_prompt_includes_git_instructions_with_autopush(self): "name": "my-repo", "url": "https://github.com/owner/my-repo.git", "branch": "main", - "autoPush": True + "autoPush": True, } ] @@ -257,7 +265,7 @@ def test_prompt_includes_git_instructions_with_autopush(self): repos_cfg=repos_cfg, workflow_name=None, artifacts_path="artifacts", - ambient_config={} + ambient_config={}, ) # Verify git instructions are present @@ -275,7 +283,7 @@ def test_prompt_excludes_git_instructions_without_autopush(self): "name": "my-repo", "url": "https://github.com/owner/my-repo.git", "branch": "main", - "autoPush": False + "autoPush": False, } ] @@ -283,7 +291,7 @@ def test_prompt_excludes_git_instructions_without_autopush(self): repos_cfg=repos_cfg, workflow_name=None, artifacts_path="artifacts", - ambient_config={} + ambient_config={}, ) # Verify git instructions are NOT present @@ -299,27 +307,27 @@ def test_prompt_includes_multiple_autopush_repos(self): "name": "repo1", "url": "https://github.com/owner/repo1.git", "branch": "main", - "autoPush": True + "autoPush": True, }, { "name": "repo2", "url": "https://github.com/owner/repo2.git", "branch": "develop", - "autoPush": True + "autoPush": True, }, { "name": "repo3", "url": "https://github.com/owner/repo3.git", "branch": "feature", - "autoPush": False - } + "autoPush": False, + }, ] prompt = self.adapter._build_workspace_context_prompt( repos_cfg=repos_cfg, workflow_name=None, artifacts_path="artifacts", - ambient_config={} + ambient_config={}, ) # Verify both autoPush repos are listed @@ -336,7 +344,7 @@ def test_prompt_without_repos(self): repos_cfg=[], workflow_name=None, artifacts_path="artifacts", - ambient_config={} + ambient_config={}, ) # Should not include git instructions @@ -352,7 +360,7 @@ def test_prompt_with_workflow(self): "name": "my-repo", "url": "https://github.com/owner/my-repo.git", "branch": "main", - "autoPush": True + "autoPush": True, } ] @@ -360,7 +368,7 @@ def test_prompt_with_workflow(self): repos_cfg=repos_cfg, workflow_name="test-workflow", artifacts_path="artifacts", - ambient_config={} + ambient_config={}, ) # Should include both workflow info and git instructions diff --git a/components/runners/claude-code-runner/tests/test_duplicate_turn_prevention.py b/components/runners/claude-code-runner/tests/test_duplicate_turn_prevention.py index a9ce089e1..9fddee7a9 100644 --- a/components/runners/claude-code-runner/tests/test_duplicate_turn_prevention.py +++ b/components/runners/claude-code-runner/tests/test_duplicate_turn_prevention.py @@ -1,7 +1,9 @@ """Unit tests for duplicate turn prevention in observability module.""" +from unittest.mock import MagicMock, Mock, patch + import pytest -from unittest.mock import Mock, patch, MagicMock + from observability import ObservabilityManager @@ -102,7 +104,9 @@ async def test_end_turn_adds_turn_number_to_metadata(self): mock_message.content = [] # End turn with SDK's turn number - manager.end_turn(5, mock_message, usage={"input_tokens": 100, "output_tokens": 50}) + manager.end_turn( + 5, mock_message, usage={"input_tokens": 100, "output_tokens": 50} + ) # Check that update was called with turn number in metadata mock_generation.update.assert_called_once() @@ -142,7 +146,9 @@ async def test_no_prediction_just_sdk_turn_count(self): mock_message = MagicMock() mock_message.content = [] - manager.end_turn(2, mock_message, usage={"input_tokens": 100, "output_tokens": 50}) + manager.end_turn( + 2, mock_message, usage={"input_tokens": 100, "output_tokens": 50} + ) # Check turn number was added to metadata call_kwargs = mock_generation.update.call_args[1] diff --git a/components/runners/claude-code-runner/tests/test_langfuse_model_metadata.py b/components/runners/claude-code-runner/tests/test_langfuse_model_metadata.py index 3b941998d..490c674a4 100644 --- a/components/runners/claude-code-runner/tests/test_langfuse_model_metadata.py +++ b/components/runners/claude-code-runner/tests/test_langfuse_model_metadata.py @@ -9,9 +9,9 @@ """ import asyncio +import logging import os import sys -import logging from pathlib import Path # Add parent directory to path @@ -37,48 +37,48 @@ async def test_model_metadata_tracking(): "claude-sonnet-4-5@20250929", "claude-haiku-4-5@20251001", "claude-opus-4-1@20250805", - None # Test with no model specified + None, # Test with no model specified ] for test_model in test_models: print(f"\n{'='*60}") print(f"Testing with model: {test_model}") - print('='*60) + print("=" * 60) # Create observability manager obs = ObservabilityManager( session_id=f"{session_id}-{test_model or 'default'}", user_id=user_id, - user_name=user_name + user_name=user_name, ) # Initialize with model metadata success = await obs.initialize( - prompt=prompt, - namespace=namespace, - model=test_model + prompt=prompt, namespace=namespace, model=test_model ) if success: print(f"✓ Observability initialized successfully with model: {test_model}") # Simulate tracking an interaction - test_message = type('Message', (), { - 'content': [type('TextBlock', (), {'text': 'Test response'})] - })() + test_message = type( + "Message", + (), + {"content": [type("TextBlock", (), {"text": "Test response"})]}, + )() test_usage = { - 'input_tokens': 100, - 'output_tokens': 50, - 'cache_read_input_tokens': 20, - 'cache_creation_input_tokens': 10 + "input_tokens": 100, + "output_tokens": 50, + "cache_read_input_tokens": 20, + "cache_creation_input_tokens": 10, } obs.track_interaction( message=test_message, - model=test_model or 'claude-sonnet-4-5@20250929', + model=test_model or "claude-sonnet-4-5@20250929", turn_count=1, - usage=test_usage + usage=test_usage, ) print(f"✓ Tracked interaction with model: {test_model}") @@ -88,27 +88,31 @@ async def test_model_metadata_tracking(): print(f"✓ Finalized observability session") else: - print(f"✗ Failed to initialize observability (expected if Langfuse not configured)") + print( + f"✗ Failed to initialize observability (expected if Langfuse not configured)" + ) - print("\n" + "="*60) + print("\n" + "=" * 60) print("Model metadata tracking test completed!") print("Check Langfuse UI to verify:") print("1. Sessions are grouped by session_id") print("2. Model appears in session metadata") print("3. Model is shown for each generation") - print("="*60) + print("=" * 60) async def test_propagate_attributes_with_model(): """Test that propagate_attributes includes model in metadata.""" # This test requires Langfuse to be configured - if not all([ - os.getenv("LANGFUSE_ENABLED") == "true", - os.getenv("LANGFUSE_PUBLIC_KEY"), - os.getenv("LANGFUSE_SECRET_KEY"), - os.getenv("LANGFUSE_HOST") - ]): + if not all( + [ + os.getenv("LANGFUSE_ENABLED") == "true", + os.getenv("LANGFUSE_PUBLIC_KEY"), + os.getenv("LANGFUSE_SECRET_KEY"), + os.getenv("LANGFUSE_HOST"), + ] + ): print("Skipping propagate_attributes test - Langfuse not configured") print("To run this test, set:") print(" LANGFUSE_ENABLED=true") @@ -123,36 +127,33 @@ async def test_propagate_attributes_with_model(): client = Langfuse( public_key=os.getenv("LANGFUSE_PUBLIC_KEY"), secret_key=os.getenv("LANGFUSE_SECRET_KEY"), - host=os.getenv("LANGFUSE_HOST") + host=os.getenv("LANGFUSE_HOST"), ) # Test propagate_attributes with model in metadata test_model = "claude-sonnet-4-5@20250929" - with client.start_as_current_observation(as_type="trace", name="test-model-metadata"): + with client.start_as_current_observation( + as_type="trace", name="test-model-metadata" + ): with propagate_attributes( user_id="test-user", session_id="test-session-propagate", metadata={ "model": test_model, "namespace": "test", - "test_type": "model_metadata" + "test_type": "model_metadata", }, - tags=["model-metadata-test", f"model:{test_model}"] + tags=["model-metadata-test", f"model:{test_model}"], ): # Create a generation that should inherit the metadata with client.start_as_current_observation( - as_type="generation", - name="test-generation", - model=test_model + as_type="generation", name="test-generation", model=test_model ) as gen: gen.update( input="Test input", output="Test output with model metadata", - usage_details={ - "input": 10, - "output": 20 - } + usage_details={"input": 10, "output": 20}, ) # Flush data @@ -163,8 +164,8 @@ async def test_propagate_attributes_with_model(): if __name__ == "__main__": print("Testing Langfuse Model Metadata Tracking") - print("="*60) + print("=" * 60) # Run tests asyncio.run(test_model_metadata_tracking()) - asyncio.run(test_propagate_attributes_with_model()) \ No newline at end of file + asyncio.run(test_propagate_attributes_with_model()) diff --git a/components/runners/claude-code-runner/tests/test_model_mapping.py b/components/runners/claude-code-runner/tests/test_model_mapping.py index 8c2526947..a9426ba7b 100644 --- a/components/runners/claude-code-runner/tests/test_model_mapping.py +++ b/components/runners/claude-code-runner/tests/test_model_mapping.py @@ -5,9 +5,10 @@ to Vertex AI model identifiers. """ -import pytest -from pathlib import Path import sys +from pathlib import Path + +import pytest # Add parent directory to path for importing wrapper module wrapper_dir = Path(__file__).parent.parent @@ -23,64 +24,64 @@ class TestMapToVertexModel: def test_map_opus_4_5(self): """Test mapping for Claude Opus 4.5""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('claude-opus-4-5') - assert result == 'claude-opus-4-5@20251101' + result = adapter._map_to_vertex_model("claude-opus-4-5") + assert result == "claude-opus-4-5@20251101" def test_map_opus_4_1(self): """Test mapping for Claude Opus 4.1""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('claude-opus-4-1') - assert result == 'claude-opus-4-1@20250805' + result = adapter._map_to_vertex_model("claude-opus-4-1") + assert result == "claude-opus-4-1@20250805" def test_map_sonnet_4_5(self): """Test mapping for Claude Sonnet 4.5""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('claude-sonnet-4-5') - assert result == 'claude-sonnet-4-5@20250929' + result = adapter._map_to_vertex_model("claude-sonnet-4-5") + assert result == "claude-sonnet-4-5@20250929" def test_map_haiku_4_5(self): """Test mapping for Claude Haiku 4.5""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('claude-haiku-4-5') - assert result == 'claude-haiku-4-5@20251001' + result = adapter._map_to_vertex_model("claude-haiku-4-5") + assert result == "claude-haiku-4-5@20251001" def test_unknown_model_returns_unchanged(self): """Test that unknown model names are returned unchanged""" adapter = ClaudeCodeAdapter() - unknown_model = 'claude-unknown-model-99' + unknown_model = "claude-unknown-model-99" result = adapter._map_to_vertex_model(unknown_model) assert result == unknown_model def test_empty_string_returns_unchanged(self): """Test that empty string is returned unchanged""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('') - assert result == '' + result = adapter._map_to_vertex_model("") + assert result == "" def test_case_sensitive_mapping(self): """Test that model mapping is case-sensitive""" adapter = ClaudeCodeAdapter() # Uppercase should not match - result = adapter._map_to_vertex_model('CLAUDE-OPUS-4-1') - assert result == 'CLAUDE-OPUS-4-1' # Should return unchanged + result = adapter._map_to_vertex_model("CLAUDE-OPUS-4-1") + assert result == "CLAUDE-OPUS-4-1" # Should return unchanged def test_whitespace_in_model_name(self): """Test handling of whitespace in model names""" adapter = ClaudeCodeAdapter() # Model name with whitespace should not match - result = adapter._map_to_vertex_model(' claude-opus-4-1 ') - assert result == ' claude-opus-4-1 ' # Should return unchanged + result = adapter._map_to_vertex_model(" claude-opus-4-1 ") + assert result == " claude-opus-4-1 " # Should return unchanged def test_partial_model_name_no_match(self): """Test that partial model names don't match""" adapter = ClaudeCodeAdapter() - result = adapter._map_to_vertex_model('claude-opus') - assert result == 'claude-opus' # Should return unchanged + result = adapter._map_to_vertex_model("claude-opus") + assert result == "claude-opus" # Should return unchanged def test_vertex_model_id_passthrough(self): """Test that Vertex AI model IDs are returned unchanged""" adapter = ClaudeCodeAdapter() - vertex_id = 'claude-opus-4-1@20250805' + vertex_id = "claude-opus-4-1@20250805" result = adapter._map_to_vertex_model(vertex_id) # If already a Vertex ID, should return unchanged assert result == vertex_id @@ -91,38 +92,50 @@ def test_all_frontend_models_have_mapping(self): # These are the exact model values from the frontend dropdown frontend_models = [ - 'claude-sonnet-4-5', - 'claude-opus-4-5', - 'claude-opus-4-1', - 'claude-haiku-4-5', + "claude-sonnet-4-5", + "claude-opus-4-5", + "claude-opus-4-1", + "claude-haiku-4-5", ] expected_mappings = { - 'claude-sonnet-4-5': 'claude-sonnet-4-5@20250929', - 'claude-opus-4-5': 'claude-opus-4-5@20251101', - 'claude-opus-4-1': 'claude-opus-4-1@20250805', - 'claude-haiku-4-5': 'claude-haiku-4-5@20251001', + "claude-sonnet-4-5": "claude-sonnet-4-5@20250929", + "claude-opus-4-5": "claude-opus-4-5@20251101", + "claude-opus-4-1": "claude-opus-4-1@20250805", + "claude-haiku-4-5": "claude-haiku-4-5@20251001", } for model in frontend_models: result = adapter._map_to_vertex_model(model) - assert result == expected_mappings[model], \ - f"Model {model} should map to {expected_mappings[model]}, got {result}" + assert ( + result == expected_mappings[model] + ), f"Model {model} should map to {expected_mappings[model]}, got {result}" def test_mapping_includes_version_date(self): """Test that all mapped models include version dates""" adapter = ClaudeCodeAdapter() - models = ['claude-opus-4-5', 'claude-opus-4-1', 'claude-sonnet-4-5', 'claude-haiku-4-5'] + models = [ + "claude-opus-4-5", + "claude-opus-4-1", + "claude-sonnet-4-5", + "claude-haiku-4-5", + ] for model in models: result = adapter._map_to_vertex_model(model) # All Vertex AI models should have @YYYYMMDD format - assert '@' in result, f"Mapped model {result} should include @ version date" - assert len(result.split('@')) == 2, f"Mapped model {result} should have exactly one @" - version_date = result.split('@')[1] - assert len(version_date) == 8, f"Version date {version_date} should be 8 digits (YYYYMMDD)" - assert version_date.isdigit(), f"Version date {version_date} should be all digits" + assert "@" in result, f"Mapped model {result} should include @ version date" + assert ( + len(result.split("@")) == 2 + ), f"Mapped model {result} should have exactly one @" + version_date = result.split("@")[1] + assert ( + len(version_date) == 8 + ), f"Version date {version_date} should be 8 digits (YYYYMMDD)" + assert ( + version_date.isdigit() + ), f"Version date {version_date} should be all digits" def test_none_input_handling(self): """Test that None input raises TypeError (invalid type per signature)""" @@ -141,14 +154,14 @@ def test_numeric_input_handling(self): def test_mapping_consistency(self): """Test that mapping is consistent across multiple calls""" adapter = ClaudeCodeAdapter() - model = 'claude-sonnet-4-5' + model = "claude-sonnet-4-5" # Call multiple times results = [adapter._map_to_vertex_model(model) for _ in range(5)] # All results should be identical assert all(r == results[0] for r in results) - assert results[0] == 'claude-sonnet-4-5@20250929' + assert results[0] == "claude-sonnet-4-5@20250929" class TestModelMappingIntegration: @@ -160,16 +173,17 @@ def test_mapping_matches_available_vertex_models(self): # Expected Vertex AI model ID format: model-name@YYYYMMDD models_to_test = [ - ('claude-opus-4-5', 'claude-opus-4-5@20251101'), - ('claude-opus-4-1', 'claude-opus-4-1@20250805'), - ('claude-sonnet-4-5', 'claude-sonnet-4-5@20250929'), - ('claude-haiku-4-5', 'claude-haiku-4-5@20251001'), + ("claude-opus-4-5", "claude-opus-4-5@20251101"), + ("claude-opus-4-1", "claude-opus-4-1@20250805"), + ("claude-sonnet-4-5", "claude-sonnet-4-5@20250929"), + ("claude-haiku-4-5", "claude-haiku-4-5@20251001"), ] for input_model, expected_vertex_id in models_to_test: result = adapter._map_to_vertex_model(input_model) - assert result == expected_vertex_id, \ - f"Expected {input_model} to map to {expected_vertex_id}, got {result}" + assert ( + result == expected_vertex_id + ), f"Expected {input_model} to map to {expected_vertex_id}, got {result}" def test_ui_to_vertex_round_trip(self): """Test that UI model selection properly maps to Vertex AI""" @@ -177,21 +191,21 @@ def test_ui_to_vertex_round_trip(self): # Simulate user selecting from UI dropdown ui_selections = [ - 'claude-sonnet-4-5', # User selects Sonnet 4.5 - 'claude-opus-4-5', # User selects Opus 4.5 - 'claude-opus-4-1', # User selects Opus 4.1 - 'claude-haiku-4-5', # User selects Haiku 4.5 + "claude-sonnet-4-5", # User selects Sonnet 4.5 + "claude-opus-4-5", # User selects Opus 4.5 + "claude-opus-4-1", # User selects Opus 4.1 + "claude-haiku-4-5", # User selects Haiku 4.5 ] for selection in ui_selections: vertex_model = adapter._map_to_vertex_model(selection) # Verify it maps to a valid Vertex AI model ID - assert vertex_model.startswith('claude-') - assert '@' in vertex_model + assert vertex_model.startswith("claude-") + assert "@" in vertex_model # Verify the base model name is preserved - base_name = vertex_model.split('@')[0] + base_name = vertex_model.split("@")[0] assert selection in vertex_model or base_name in selection def test_end_to_end_vertex_mapping_flow(self): @@ -201,44 +215,45 @@ def test_end_to_end_vertex_mapping_flow(self): # Simulate complete flow for each model test_scenarios = [ { - 'ui_selection': 'claude-opus-4-5', - 'expected_vertex_id': 'claude-opus-4-5@20251101', - 'description': 'Latest Opus model', + "ui_selection": "claude-opus-4-5", + "expected_vertex_id": "claude-opus-4-5@20251101", + "description": "Latest Opus model", }, { - 'ui_selection': 'claude-opus-4-1', - 'expected_vertex_id': 'claude-opus-4-1@20250805', - 'description': 'Previous Opus model', + "ui_selection": "claude-opus-4-1", + "expected_vertex_id": "claude-opus-4-1@20250805", + "description": "Previous Opus model", }, { - 'ui_selection': 'claude-sonnet-4-5', - 'expected_vertex_id': 'claude-sonnet-4-5@20250929', - 'description': 'Balanced model', + "ui_selection": "claude-sonnet-4-5", + "expected_vertex_id": "claude-sonnet-4-5@20250929", + "description": "Balanced model", }, { - 'ui_selection': 'claude-haiku-4-5', - 'expected_vertex_id': 'claude-haiku-4-5@20251001', - 'description': 'Fastest model', + "ui_selection": "claude-haiku-4-5", + "expected_vertex_id": "claude-haiku-4-5@20251001", + "description": "Fastest model", }, ] for scenario in test_scenarios: # Step 1: User selects model from UI - ui_model = scenario['ui_selection'] + ui_model = scenario["ui_selection"] # Step 2: Backend maps to Vertex AI model ID vertex_model_id = adapter._map_to_vertex_model(ui_model) # Step 3: Verify correct mapping - assert vertex_model_id == scenario['expected_vertex_id'], \ - f"{scenario['description']}: Expected {scenario['expected_vertex_id']}, got {vertex_model_id}" + assert ( + vertex_model_id == scenario["expected_vertex_id"] + ), f"{scenario['description']}: Expected {scenario['expected_vertex_id']}, got {vertex_model_id}" # Step 4: Verify Vertex AI model ID format is valid - assert '@' in vertex_model_id - parts = vertex_model_id.split('@') + assert "@" in vertex_model_id + parts = vertex_model_id.split("@") assert len(parts) == 2 model_name, version_date = parts - assert model_name.startswith('claude-') + assert model_name.startswith("claude-") assert len(version_date) == 8 # YYYYMMDD format assert version_date.isdigit() @@ -248,19 +263,19 @@ def test_model_ordering_consistency(self): # Expected ordering: Sonnet → Opus 4.5 → Opus 4.1 → Haiku (matches frontend dropdown) expected_order = [ - 'claude-sonnet-4-5', - 'claude-opus-4-5', - 'claude-opus-4-1', - 'claude-haiku-4-5', + "claude-sonnet-4-5", + "claude-opus-4-5", + "claude-opus-4-1", + "claude-haiku-4-5", ] # Verify all models map successfully in order for model in expected_order: vertex_id = adapter._map_to_vertex_model(model) - assert '@' in vertex_id, f"Model {model} should map to valid Vertex AI ID" + assert "@" in vertex_id, f"Model {model} should map to valid Vertex AI ID" # Verify ordering matches frontend dropdown - assert expected_order[0] == 'claude-sonnet-4-5' # Balanced (default) - assert expected_order[1] == 'claude-opus-4-5' # Latest Opus - assert expected_order[2] == 'claude-opus-4-1' # Previous Opus - assert expected_order[3] == 'claude-haiku-4-5' # Fastest + assert expected_order[0] == "claude-sonnet-4-5" # Balanced (default) + assert expected_order[1] == "claude-opus-4-5" # Latest Opus + assert expected_order[2] == "claude-opus-4-1" # Previous Opus + assert expected_order[3] == "claude-haiku-4-5" # Fastest diff --git a/components/runners/claude-code-runner/tests/test_observability.py b/components/runners/claude-code-runner/tests/test_observability.py index 0065a2188..feb91f25a 100644 --- a/components/runners/claude-code-runner/tests/test_observability.py +++ b/components/runners/claude-code-runner/tests/test_observability.py @@ -1,9 +1,11 @@ """Unit tests for observability module.""" -import pytest -import os import logging +import os from unittest.mock import Mock, patch + +import pytest + from observability import ObservabilityManager, _privacy_masking_function @@ -53,8 +55,11 @@ class TestLangfuseInitialization: async def test_init_langfuse_unavailable(self, manager): """Test initialization when Langfuse SDK is not available.""" # Mock the import to raise ImportError - with patch.dict('sys.modules', {'langfuse': None}): - with patch('builtins.__import__', side_effect=ImportError("No module named 'langfuse'")): + with patch.dict("sys.modules", {"langfuse": None}): + with patch( + "builtins.__import__", + side_effect=ImportError("No module named 'langfuse'"), + ): result = await manager.initialize("test prompt", "test-namespace") assert result is False @@ -125,7 +130,9 @@ async def test_init_missing_host(self, manager, caplog): @pytest.mark.asyncio @patch("langfuse.propagate_attributes") @patch("langfuse.Langfuse") - async def test_init_successful(self, mock_langfuse_class, mock_propagate, manager, caplog): + async def test_init_successful( + self, mock_langfuse_class, mock_propagate, manager, caplog + ): """Test successful Langfuse initialization with SDK v3 propagate_attributes pattern.""" mock_client = Mock() mock_langfuse_class.return_value = mock_client @@ -171,7 +178,9 @@ async def test_init_successful(self, mock_langfuse_class, mock_propagate, manage @pytest.mark.asyncio @patch("langfuse.propagate_attributes") @patch("langfuse.Langfuse") - async def test_init_with_user_tracking(self, mock_langfuse_class, mock_propagate, caplog): + async def test_init_with_user_tracking( + self, mock_langfuse_class, mock_propagate, caplog + ): """Test Langfuse initialization with user tracking.""" mock_client = Mock() mock_langfuse_class.return_value = mock_client @@ -298,11 +307,15 @@ def test_start_turn_prevents_duplicate_traces(self, manager): # Second call to start_turn (same turn, streaming update) - should be ignored manager.start_turn("claude-3-5-sonnet", "User input") - assert mock_client.start_as_current_observation.call_count == 1 # Still 1, not 2 + assert ( + mock_client.start_as_current_observation.call_count == 1 + ) # Still 1, not 2 # Third call to start_turn (still same turn) - should be ignored manager.start_turn("claude-3-5-sonnet", "User input") - assert mock_client.start_as_current_observation.call_count == 1 # Still 1, not 3 + assert ( + mock_client.start_as_current_observation.call_count == 1 + ) # Still 1, not 3 class TestEndTurn: diff --git a/components/runners/claude-code-runner/tests/test_privacy_masking.py b/components/runners/claude-code-runner/tests/test_privacy_masking.py index b58f215e0..56471c55a 100644 --- a/components/runners/claude-code-runner/tests/test_privacy_masking.py +++ b/components/runners/claude-code-runner/tests/test_privacy_masking.py @@ -37,7 +37,7 @@ def test_dict_usage_preservation(): "cache_read_input_tokens": 200, "cache_creation_input_tokens": 100, "total_tokens": 1800, - "cost_usd": 0.05 + "cost_usd": 0.05, } masked = _privacy_masking_function(usage_data) @@ -57,7 +57,7 @@ def test_dict_content_masking(): "role": "user", "content": "Please help me analyze this confidential document with sensitive customer data", "model": "claude-3-5-sonnet", - "turn": 1 + "turn": 1, } masked = _privacy_masking_function(message_data) @@ -77,19 +77,16 @@ def test_nested_structure(): "input": [ { "role": "user", - "content": "Here is my confidential business plan with trade secrets and financial projections" + "content": "Here is my confidential business plan with trade secrets and financial projections", } ], "output": "Based on your business plan, I recommend the following strategies for growth and expansion", - "usage": { - "input_tokens": 500, - "output_tokens": 250 - }, + "usage": {"input_tokens": 500, "output_tokens": 250}, "metadata": { "model": "claude-sonnet-4-5@20250929", "turn": 2, - "session_id": "session-123" - } + "session_id": "session-123", + }, } masked = _privacy_masking_function(trace_data) @@ -116,7 +113,7 @@ def test_list_masking(): messages = [ "Short metadata value", "This is a very long user message that contains sensitive personal information about the user", - {"content": "Another sensitive message in a nested dictionary structure"} + {"content": "Another sensitive message in a nested dictionary structure"}, ] masked = _privacy_masking_function(messages) @@ -136,16 +133,12 @@ def test_tool_tracking_data(): tool_data = { "tool_name": "Read", "tool_id": "toolu_abc123", - "input": { - "file_path": "/workspace/src/main.py" - }, + "input": {"file_path": "/workspace/src/main.py"}, "output": { "result": "File contents here with potentially sensitive code and comments about implementation details" }, "is_error": False, - "metadata": { - "turn": 3 - } + "metadata": {"turn": 3}, } masked = _privacy_masking_function(tool_data) @@ -186,7 +179,7 @@ def test_real_world_trace(): "input": [ { "role": "user", - "content": "Can you help me refactor this legacy codebase? It has several security vulnerabilities." + "content": "Can you help me refactor this legacy codebase? It has several security vulnerabilities.", } ], "output": "I'll help you refactor the codebase. First, let me analyze the current structure and identify the security issues.", @@ -195,13 +188,13 @@ def test_real_world_trace(): "input": 150, "output": 75, "cache_read_input_tokens": 50, - "cache_creation_input_tokens": 25 + "cache_creation_input_tokens": 25, }, "metadata": { "turn": 1, "session_id": "test-session-123", - "namespace": "prod-namespace" - } + "namespace": "prod-namespace", + }, } masked = _privacy_masking_function(trace) @@ -265,4 +258,6 @@ def test_real_world_trace(): sys.exit(1) else: print("\n✅ All privacy masking tests passed!") - print("User messages and responses will be redacted while preserving usage metrics.") + print( + "User messages and responses will be redacted while preserving usage metrics." + ) diff --git a/components/runners/claude-code-runner/tests/test_security_utils.py b/components/runners/claude-code-runner/tests/test_security_utils.py index d3edabb54..9cbafd40e 100644 --- a/components/runners/claude-code-runner/tests/test_security_utils.py +++ b/components/runners/claude-code-runner/tests/test_security_utils.py @@ -1,14 +1,16 @@ """Unit tests for security_utils module.""" -import pytest import asyncio import logging + +import pytest + from security_utils import ( sanitize_exception_message, sanitize_model_name, - with_timeout, - with_sync_timeout, validate_and_sanitize_for_logging, + with_sync_timeout, + with_timeout, ) @@ -336,10 +338,22 @@ class TestSanitizeModelName: def test_valid_claude_model_names(self): """Test that valid Claude model names pass through.""" - assert sanitize_model_name("claude-3-5-sonnet-20241022") == "claude-3-5-sonnet-20241022" - assert sanitize_model_name("claude-sonnet-4-5@20250929") == "claude-sonnet-4-5@20250929" - assert sanitize_model_name("claude-opus-4-1@20250805") == "claude-opus-4-1@20250805" - assert sanitize_model_name("claude-haiku-4-5@20251001") == "claude-haiku-4-5@20251001" + assert ( + sanitize_model_name("claude-3-5-sonnet-20241022") + == "claude-3-5-sonnet-20241022" + ) + assert ( + sanitize_model_name("claude-sonnet-4-5@20250929") + == "claude-sonnet-4-5@20250929" + ) + assert ( + sanitize_model_name("claude-opus-4-1@20250805") + == "claude-opus-4-1@20250805" + ) + assert ( + sanitize_model_name("claude-haiku-4-5@20251001") + == "claude-haiku-4-5@20251001" + ) def test_valid_other_model_names(self): """Test that other valid model names pass through.""" @@ -349,8 +363,13 @@ def test_valid_other_model_names(self): def test_removes_invalid_characters(self): """Test that invalid characters are removed.""" - assert sanitize_model_name("claude-3") == "claude-3scriptalertxss/script" - assert sanitize_model_name("model;DROP TABLE users;--") == "modelDROPTABLEusers--" + assert ( + sanitize_model_name("claude-3") + == "claude-3scriptalertxss/script" + ) + assert ( + sanitize_model_name("model;DROP TABLE users;--") == "modelDROPTABLEusers--" + ) assert sanitize_model_name("model\n\r\t\x00") == "model" assert sanitize_model_name("model with spaces") == "modelwithspaces" @@ -393,4 +412,7 @@ def test_injection_attack_prevention(self): # Path traversal attempt assert sanitize_model_name("../../etc/passwd") == "../../etc/passwd" # JavaScript injection - assert sanitize_model_name("") == "scriptalertxss/script" + assert ( + sanitize_model_name("") + == "scriptalertxss/script" + ) diff --git a/components/runners/claude-code-runner/tests/test_wrapper_vertex.py b/components/runners/claude-code-runner/tests/test_wrapper_vertex.py index 979b8e7ac..bd14180e3 100644 --- a/components/runners/claude-code-runner/tests/test_wrapper_vertex.py +++ b/components/runners/claude-code-runner/tests/test_wrapper_vertex.py @@ -11,7 +11,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest - from claude_code_runner.wrapper import ClaudeCodeWrapper @@ -29,7 +28,7 @@ def mock_context(self): @pytest.fixture def temp_credentials_file(self): """Create a temporary credentials file""" - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write('{"test": "credentials"}') temp_path = f.name yield temp_path @@ -38,13 +37,15 @@ def temp_credentials_file(self): os.unlink(temp_path) @pytest.mark.asyncio - async def test_success_all_valid_credentials(self, mock_context, temp_credentials_file): + async def test_success_all_valid_credentials( + self, mock_context, temp_credentials_file + ): """Test successful setup with all valid credentials""" # Setup mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -54,9 +55,9 @@ async def test_success_all_valid_credentials(self, mock_context, temp_credential # Verify assert result is not None - assert result['credentials_path'] == temp_credentials_file - assert result['project_id'] == 'test-project-123' - assert result['region'] == 'us-central1' + assert result["credentials_path"] == temp_credentials_file + assert result["project_id"] == "test-project-123" + assert result["region"] == "us-central1" # Verify logging was called mock_context.send_log.assert_called() @@ -66,8 +67,8 @@ async def test_error_missing_google_application_credentials(self, mock_context): """Test error when GOOGLE_APPLICATION_CREDENTIALS is not set""" # Setup - missing GOOGLE_APPLICATION_CREDENTIALS mock_context.get_env.side_effect = lambda key: { - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -76,17 +77,17 @@ async def test_error_missing_google_application_credentials(self, mock_context): with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'GOOGLE_APPLICATION_CREDENTIALS' in str(exc_info.value) - assert 'not set' in str(exc_info.value) + assert "GOOGLE_APPLICATION_CREDENTIALS" in str(exc_info.value) + assert "not set" in str(exc_info.value) @pytest.mark.asyncio async def test_error_empty_google_application_credentials(self, mock_context): """Test error when GOOGLE_APPLICATION_CREDENTIALS is empty string""" # Setup - empty string mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': '', - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": "", + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -95,15 +96,17 @@ async def test_error_empty_google_application_credentials(self, mock_context): with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'GOOGLE_APPLICATION_CREDENTIALS' in str(exc_info.value) + assert "GOOGLE_APPLICATION_CREDENTIALS" in str(exc_info.value) @pytest.mark.asyncio - async def test_error_missing_anthropic_vertex_project_id(self, mock_context, temp_credentials_file): + async def test_error_missing_anthropic_vertex_project_id( + self, mock_context, temp_credentials_file + ): """Test error when ANTHROPIC_VERTEX_PROJECT_ID is not set""" # Setup - missing ANTHROPIC_VERTEX_PROJECT_ID mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -112,17 +115,19 @@ async def test_error_missing_anthropic_vertex_project_id(self, mock_context, tem with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'ANTHROPIC_VERTEX_PROJECT_ID' in str(exc_info.value) - assert 'not set' in str(exc_info.value) + assert "ANTHROPIC_VERTEX_PROJECT_ID" in str(exc_info.value) + assert "not set" in str(exc_info.value) @pytest.mark.asyncio - async def test_error_empty_anthropic_vertex_project_id(self, mock_context, temp_credentials_file): + async def test_error_empty_anthropic_vertex_project_id( + self, mock_context, temp_credentials_file + ): """Test error when ANTHROPIC_VERTEX_PROJECT_ID is empty string""" # Setup - empty string mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': '', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -131,15 +136,17 @@ async def test_error_empty_anthropic_vertex_project_id(self, mock_context, temp_ with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'ANTHROPIC_VERTEX_PROJECT_ID' in str(exc_info.value) + assert "ANTHROPIC_VERTEX_PROJECT_ID" in str(exc_info.value) @pytest.mark.asyncio - async def test_error_missing_cloud_ml_region(self, mock_context, temp_credentials_file): + async def test_error_missing_cloud_ml_region( + self, mock_context, temp_credentials_file + ): """Test error when CLOUD_ML_REGION is not set""" # Setup - missing CLOUD_ML_REGION mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -148,17 +155,19 @@ async def test_error_missing_cloud_ml_region(self, mock_context, temp_credential with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'CLOUD_ML_REGION' in str(exc_info.value) - assert 'not set' in str(exc_info.value) + assert "CLOUD_ML_REGION" in str(exc_info.value) + assert "not set" in str(exc_info.value) @pytest.mark.asyncio - async def test_error_empty_cloud_ml_region(self, mock_context, temp_credentials_file): + async def test_error_empty_cloud_ml_region( + self, mock_context, temp_credentials_file + ): """Test error when CLOUD_ML_REGION is empty string""" # Setup - empty string mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': '', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -167,17 +176,17 @@ async def test_error_empty_cloud_ml_region(self, mock_context, temp_credentials_ with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'CLOUD_ML_REGION' in str(exc_info.value) + assert "CLOUD_ML_REGION" in str(exc_info.value) @pytest.mark.asyncio async def test_error_credentials_file_does_not_exist(self, mock_context): """Test error when service account file doesn't exist""" # Setup - path to non-existent file - non_existent_path = '/tmp/non_existent_credentials_file_12345.json' + non_existent_path = "/tmp/non_existent_credentials_file_12345.json" mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': non_existent_path, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": non_existent_path, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -186,8 +195,8 @@ async def test_error_credentials_file_does_not_exist(self, mock_context): with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'Service account file' in str(exc_info.value) - assert 'does not exist' in str(exc_info.value) + assert "Service account file" in str(exc_info.value) + assert "does not exist" in str(exc_info.value) assert non_existent_path in str(exc_info.value) @pytest.mark.asyncio @@ -202,15 +211,15 @@ async def test_error_all_env_vars_missing(self, mock_context): with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'GOOGLE_APPLICATION_CREDENTIALS' in str(exc_info.value) + assert "GOOGLE_APPLICATION_CREDENTIALS" in str(exc_info.value) @pytest.mark.asyncio async def test_validation_order_checks_credentials_path_first(self, mock_context): """Test that validation checks occur in correct order (credentials path first)""" # Setup - credentials missing, other vars present mock_context.get_env.side_effect = lambda key: { - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -219,15 +228,17 @@ async def test_validation_order_checks_credentials_path_first(self, mock_context with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'GOOGLE_APPLICATION_CREDENTIALS' in str(exc_info.value) + assert "GOOGLE_APPLICATION_CREDENTIALS" in str(exc_info.value) @pytest.mark.asyncio - async def test_validation_order_checks_project_id_second(self, mock_context, temp_credentials_file): + async def test_validation_order_checks_project_id_second( + self, mock_context, temp_credentials_file + ): """Test that validation checks project_id after credentials path""" # Setup - credentials present, project_id missing mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -236,15 +247,17 @@ async def test_validation_order_checks_project_id_second(self, mock_context, tem with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'ANTHROPIC_VERTEX_PROJECT_ID' in str(exc_info.value) + assert "ANTHROPIC_VERTEX_PROJECT_ID" in str(exc_info.value) @pytest.mark.asyncio - async def test_validation_order_checks_region_third(self, mock_context, temp_credentials_file): + async def test_validation_order_checks_region_third( + self, mock_context, temp_credentials_file + ): """Test that validation checks region after project_id""" # Setup - credentials and project_id present, region missing mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -253,17 +266,17 @@ async def test_validation_order_checks_region_third(self, mock_context, temp_cre with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'CLOUD_ML_REGION' in str(exc_info.value) + assert "CLOUD_ML_REGION" in str(exc_info.value) @pytest.mark.asyncio async def test_validation_checks_file_existence_last(self, mock_context): """Test that file existence is checked after all env vars""" # Setup - all env vars present but file doesn't exist - non_existent_path = '/tmp/does_not_exist_credentials.json' + non_existent_path = "/tmp/does_not_exist_credentials.json" mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': non_existent_path, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": non_existent_path, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -272,17 +285,19 @@ async def test_validation_checks_file_existence_last(self, mock_context): with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'Service account file' in str(exc_info.value) - assert 'does not exist' in str(exc_info.value) + assert "Service account file" in str(exc_info.value) + assert "does not exist" in str(exc_info.value) @pytest.mark.asyncio - async def test_logging_output_includes_config_details(self, mock_context, temp_credentials_file): + async def test_logging_output_includes_config_details( + self, mock_context, temp_credentials_file + ): """Test that successful setup logs configuration details""" # Setup mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -294,19 +309,25 @@ async def test_logging_output_includes_config_details(self, mock_context, temp_c assert mock_context.send_log.called # Check that log messages contain key info log_calls = [call.args[0] for call in mock_context.send_log.call_args_list] - log_text = ' '.join(log_calls) + log_text = " ".join(log_calls) - assert 'test-project-123' in log_text or any('project' in call.lower() for call in log_calls) - assert 'us-central1' in log_text or any('region' in call.lower() for call in log_calls) + assert "test-project-123" in log_text or any( + "project" in call.lower() for call in log_calls + ) + assert "us-central1" in log_text or any( + "region" in call.lower() for call in log_calls + ) @pytest.mark.asyncio - async def test_whitespace_in_env_vars_is_not_trimmed(self, mock_context, temp_credentials_file): + async def test_whitespace_in_env_vars_is_not_trimmed( + self, mock_context, temp_credentials_file + ): """Test that whitespace in environment variables causes validation failure""" # Setup - env vars with leading/trailing whitespace mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': ' test-project-123 ', - 'CLOUD_ML_REGION': ' us-central1 ', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": " test-project-123 ", + "CLOUD_ML_REGION": " us-central1 ", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -316,16 +337,18 @@ async def test_whitespace_in_env_vars_is_not_trimmed(self, mock_context, temp_cr result = await wrapper._setup_vertex_credentials() # Verify that whitespace is preserved (not stripped) - assert result['project_id'] == ' test-project-123 ' - assert result['region'] == ' us-central1 ' + assert result["project_id"] == " test-project-123 " + assert result["region"] == " us-central1 " @pytest.mark.asyncio async def test_none_value_from_get_env(self, mock_context, temp_credentials_file): """Test behavior when get_env returns None""" # Setup - get_env returns None for missing vars mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - }.get(key) # Returns None for other keys + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + }.get( + key + ) # Returns None for other keys wrapper = ClaudeCodeWrapper(mock_context) @@ -333,7 +356,7 @@ async def test_none_value_from_get_env(self, mock_context, temp_credentials_file with pytest.raises(RuntimeError) as exc_info: await wrapper._setup_vertex_credentials() - assert 'not set' in str(exc_info.value) + assert "not set" in str(exc_info.value) @pytest.mark.asyncio async def test_directory_instead_of_file(self, mock_context, tmp_path): @@ -343,9 +366,9 @@ async def test_directory_instead_of_file(self, mock_context, tmp_path): dir_path.mkdir() mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': str(dir_path), - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": str(dir_path), + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -363,15 +386,17 @@ async def test_directory_instead_of_file(self, mock_context, tmp_path): async def test_relative_path_credentials_file(self, mock_context): """Test handling of relative path for credentials file""" # Setup - create a file in current directory - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, dir='.') as f: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False, dir="." + ) as f: f.write('{"test": "credentials"}') relative_path = os.path.basename(f.name) try: mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': relative_path, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": relative_path, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -380,21 +405,23 @@ async def test_relative_path_credentials_file(self, mock_context): result = await wrapper._setup_vertex_credentials() assert result is not None - assert result['credentials_path'] == relative_path + assert result["credentials_path"] == relative_path finally: # Cleanup if os.path.exists(relative_path): os.unlink(relative_path) @pytest.mark.asyncio - async def test_special_characters_in_project_id(self, mock_context, temp_credentials_file): + async def test_special_characters_in_project_id( + self, mock_context, temp_credentials_file + ): """Test handling of special characters in project ID""" # Setup - project ID with special characters - special_project_id = 'test-project-123_with-special.chars' + special_project_id = "test-project-123_with-special.chars" mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': special_project_id, - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": special_project_id, + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -403,24 +430,26 @@ async def test_special_characters_in_project_id(self, mock_context, temp_credent result = await wrapper._setup_vertex_credentials() # Should accept special characters - assert result['project_id'] == special_project_id + assert result["project_id"] == special_project_id @pytest.mark.asyncio - async def test_international_region_codes(self, mock_context, temp_credentials_file): + async def test_international_region_codes( + self, mock_context, temp_credentials_file + ): """Test handling of various region codes""" # Test multiple regions regions = [ - 'us-central1', - 'europe-west1', - 'asia-southeast1', - 'australia-southeast1', + "us-central1", + "europe-west1", + "asia-southeast1", + "australia-southeast1", ] for region in regions: mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project', - 'CLOUD_ML_REGION': region, + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project", + "CLOUD_ML_REGION": region, }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -429,16 +458,16 @@ async def test_international_region_codes(self, mock_context, temp_credentials_f result = await wrapper._setup_vertex_credentials() # Should accept all valid region codes - assert result['region'] == region + assert result["region"] == region @pytest.mark.asyncio async def test_return_value_structure(self, mock_context, temp_credentials_file): """Test that return value has expected structure""" # Setup mock_context.get_env.side_effect = lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_credentials_file, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'test-project-123', - 'CLOUD_ML_REGION': 'us-central1', + "GOOGLE_APPLICATION_CREDENTIALS": temp_credentials_file, + "ANTHROPIC_VERTEX_PROJECT_ID": "test-project-123", + "CLOUD_ML_REGION": "us-central1", }.get(key) wrapper = ClaudeCodeWrapper(mock_context) @@ -448,9 +477,9 @@ async def test_return_value_structure(self, mock_context, temp_credentials_file) # Verify structure assert isinstance(result, dict) - assert 'credentials_path' in result - assert 'project_id' in result - assert 'region' in result + assert "credentials_path" in result + assert "project_id" in result + assert "region" in result assert len(result) == 3 # Exactly these three keys @@ -461,18 +490,20 @@ class TestSetupVertexCredentialsIntegration: async def test_integration_with_real_file_creation(self): """Test with actual file creation and deletion""" # Create temporary credentials file - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write('{"type": "service_account", "project_id": "test"}') temp_path = f.name try: # Create mock context context = MagicMock() - context.get_env = MagicMock(side_effect=lambda key: { - 'GOOGLE_APPLICATION_CREDENTIALS': temp_path, - 'ANTHROPIC_VERTEX_PROJECT_ID': 'integration-test-project', - 'CLOUD_ML_REGION': 'us-west1', - }.get(key)) + context.get_env = MagicMock( + side_effect=lambda key: { + "GOOGLE_APPLICATION_CREDENTIALS": temp_path, + "ANTHROPIC_VERTEX_PROJECT_ID": "integration-test-project", + "CLOUD_ML_REGION": "us-west1", + }.get(key) + ) context.send_log = AsyncMock() wrapper = ClaudeCodeWrapper(context) @@ -482,9 +513,9 @@ async def test_integration_with_real_file_creation(self): # Verify assert Path(temp_path).exists() - assert result['credentials_path'] == temp_path - assert result['project_id'] == 'integration-test-project' - assert result['region'] == 'us-west1' + assert result["credentials_path"] == temp_path + assert result["project_id"] == "integration-test-project" + assert result["region"] == "us-west1" finally: # Cleanup @@ -502,11 +533,13 @@ async def test_concurrent_calls_to_setup_vertex_credentials(self, tmp_path): contexts = [] for i in range(5): context = MagicMock() - context.get_env = MagicMock(side_effect=lambda key, i=i: { - 'GOOGLE_APPLICATION_CREDENTIALS': str(creds_file), - 'ANTHROPIC_VERTEX_PROJECT_ID': f'project-{i}', - 'CLOUD_ML_REGION': f'region-{i}', - }.get(key)) + context.get_env = MagicMock( + side_effect=lambda key, i=i: { + "GOOGLE_APPLICATION_CREDENTIALS": str(creds_file), + "ANTHROPIC_VERTEX_PROJECT_ID": f"project-{i}", + "CLOUD_ML_REGION": f"region-{i}", + }.get(key) + ) context.send_log = AsyncMock() contexts.append(context) @@ -519,5 +552,5 @@ async def test_concurrent_calls_to_setup_vertex_credentials(self, tmp_path): # Verify all succeeded assert len(results) == 5 for i, result in enumerate(results): - assert result['project_id'] == f'project-{i}' - assert result['region'] == f'region-{i}' + assert result["project_id"] == f"project-{i}" + assert result["region"] == f"region-{i}" diff --git a/components/runners/claude-code-runner/uv.lock b/components/runners/claude-code-runner/uv.lock index 1f313abfe..0a1c1c954 100644 --- a/components/runners/claude-code-runner/uv.lock +++ b/components/runners/claude-code-runner/uv.lock @@ -1,7 +1,19 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" +[[package]] +name = "ag-ui-protocol" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -126,6 +138,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -173,6 +194,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "atlassian-python-api" +version = "4.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "deprecated" }, + { name = "jmespath" }, + { name = "oauthlib" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/e8/f23b7273e410c6fe9f98f9db25268c6736572f22a9566d1dc9ed3614bb68/atlassian_python_api-4.0.7.tar.gz", hash = "sha256:8d9cc6068b1d2a48eb434e22e57f6bbd918a47fac9e46b95b7a3cefb00fceacb", size = 271149, upload-time = "2025-08-21T13:19:40.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/83/e4f9976ce3c933a079b8931325e7a9c0a8bba7030a2cb85764c0048f3479/atlassian_python_api-4.0.7-py3-none-any.whl", hash = "sha256:46a70cb29eaab87c0a1697fccd3e25df1aa477e6aa4fb9ba936a9d46b425933c", size = 197746, upload-time = "2025-08-21T13:19:39.044Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -182,6 +230,18 @@ 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 = "authlib" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -191,6 +251,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "black" version = "25.11.0" @@ -387,35 +478,42 @@ wheels = [ [[package]] name = "claude-agent-sdk" -version = "0.1.9" +version = "0.1.23" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "mcp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/c5/ffb02e51f6fc5e5516a52f8f1e338c16830ee616caa5e140d6c0c3f77e7e/claude_agent_sdk-0.1.9.tar.gz", hash = "sha256:5dcf04a4639bd3dd76145b5067851febe515f5540abd90e3af26a4aa22ed91b1", size = 51067, upload-time = "2025-11-21T20:24:32.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/45/760b2f3292b7de21108d15bfc1fcab2da0ddbf4ff2590dd8b114f89dfe6a/claude_agent_sdk-0.1.23.tar.gz", hash = "sha256:c3002869e3e9e6868d195aaf475166a3ddef0a78b78eb1585220142657ccb3c6", size = 57112, upload-time = "2026-01-27T01:46:16.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/71/4d99c86f14754ea3998d8a59abf9db4f10498988d45fab102faa6c909fc4/claude_agent_sdk-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d692baac86d5d0dbf0729121e85bcbd390c4413b7aa5a988f7dd5aa7c4237afb", size = 49323542, upload-time = "2025-11-21T20:24:20.053Z" }, - { url = "https://files.pythonhosted.org/packages/c0/08/5894220578a000f3e2c7d02911ff4196fd9ddf1e77b26ea4b7a1be294639/claude_agent_sdk-0.1.9-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:d826a8afabc5e2d4edca5eb65771f4293a5d9df0eeebcbb5171fdce93d4fea1b", size = 65196329, upload-time = "2025-11-21T20:24:24.666Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fd/810628f5e020171cbaa48e82de14fccd66383f5e77d0f3fdd8859fef796e/claude_agent_sdk-0.1.9-py3-none-win_amd64.whl", hash = "sha256:37e0cff2d277e617c5597f7bb0eb160c12ca57409d36060f7020fc9893f6337c", size = 68083295, upload-time = "2025-11-21T20:24:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/29/bb/35f6cebabc94beaa81042fc195f2fbc6a586498d18700f657e5acd434433/claude_agent_sdk-0.1.23-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d91e1d431c5b7ba41791068d63f883b579d7314cb7cb4d9e401b68bf6440c1a2", size = 54527103, upload-time = "2026-01-27T01:46:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/28/33/0511fdf5b21d10947ab5e839a63b60034bf4cff31777e5237e9dc99fea14/claude_agent_sdk-0.1.23-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:2575f38a39e7e64a84cbd7144b4485b5cdcef88e35aab6ef42ceb919747d0e2d", size = 68705060, upload-time = "2026-01-27T01:46:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/6e/0e41f496ac979243e8bb2ee80899587377ecfda2979fb8a994861b9857b2/claude_agent_sdk-0.1.23-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:a392700455741d0ad0ef50412f6babd56657ba0ebc7a5bd26a13b82d4c4ae078", size = 70420014, upload-time = "2026-01-27T01:46:10.669Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb00bd631c4d88d9fbf1417a864c2645b992259886123c0e44ccfe87f355/claude_agent_sdk-0.1.23-py3-none-win_amd64.whl", hash = "sha256:05a44208a199cbd7c6cce034d3f91be3cb1a2b11df8e85bedb7c0e31c783d8c7", size = 72624773, upload-time = "2026-01-27T01:46:14.094Z" }, ] [[package]] name = "claude-code-runner" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ + { name = "ag-ui-protocol" }, { name = "aiohttp" }, { name = "anthropic", extra = ["vertex"] }, { name = "claude-agent-sdk" }, + { name = "fastapi" }, { name = "langfuse" }, + { name = "mcp-atlassian" }, + { name = "pydantic" }, { name = "pyjwt" }, { name = "requests" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.dev-dependencies] dev = [ { name = "black" }, + { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -424,17 +522,23 @@ dev = [ [package.metadata] requires-dist = [ + { name = "ag-ui-protocol", specifier = ">=0.1.0" }, { name = "aiohttp", specifier = ">=3.8.0" }, { name = "anthropic", extras = ["vertex"], specifier = ">=0.68.0" }, - { name = "claude-agent-sdk", specifier = ">=0.1.4" }, + { name = "claude-agent-sdk", specifier = ">=0.1.23" }, + { name = "fastapi", specifier = ">=0.100.0" }, { name = "langfuse", specifier = ">=3.0.0" }, + { name = "mcp-atlassian", specifier = ">=0.11.9" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "pyjwt", specifier = ">=2.8.0" }, { name = "requests", specifier = ">=2.31.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.23.0" }, ] [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=23.0.0" }, + { name = "httpx", specifier = ">=0.24.0" }, { name = "pytest", specifier = ">=7.4.0" }, { name = "pytest-asyncio", specifier = ">=0.21.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, @@ -453,6 +557,15 @@ 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 = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -616,6 +729,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -625,6 +783,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -634,6 +801,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "fastmcp" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/b5/7c4744dc41390ed2c17fd462ef2d42f4448a1ec53dda8fe3a01ff2872313/fastmcp-2.14.3.tar.gz", hash = "sha256:abc9113d5fcf79dfb4c060a1e1c55fccb0d4bce4a2e3eab15ca352341eec8dd6", size = 8279206, upload-time = "2026-01-12T20:00:40.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/dc/f7dd14213bf511690dccaa5094d436947c253b418c86c86211d1c76e6e44/fastmcp-2.14.3-py3-none-any.whl", hash = "sha256:103c6b4c6e97a9acc251c81d303f110fe4f2bdba31353df515d66272bf1b9414", size = 416220, upload-time = "2026-01-12T20:00:42.543Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -792,6 +1053,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -846,6 +1143,51 @@ 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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jiter" version = "0.12.0" @@ -931,6 +1273,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -946,6 +1297,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -958,6 +1324,24 @@ 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 = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "langfuse" version = "3.10.1" @@ -979,9 +1363,228 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/83/254c8d38bd46e9c49cc2814da0ece5cb361315e6728578112ffae9bffbe8/langfuse-3.10.1-py3-none-any.whl", hash = "sha256:78582905874e17f923a3fa6eba9d1a15e1547139bbd5c11d498ce90670e1fdae", size = 391696, upload-time = "2025-11-19T17:49:59.069Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdown-to-confluence" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "markdown" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "types-lxml" }, + { name = "types-markdown" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/7d/ff8876b6c36172a02941b53a7456e5628597fc0cbfc100882da2a066ec7d/markdown_to_confluence-0.3.5.tar.gz", hash = "sha256:4309af625682f6d300e117992b87e6459a8ae6b653dee2f9126a678acf076f0b", size = 43390, upload-time = "2025-06-02T13:13:13.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/10/1188c4e41167af312fc567015276b29156baf77e9ce5485837f906557e23/markdown_to_confluence-0.3.5-py3-none-any.whl", hash = "sha256:74469710a25d45242827b2143fdb00be3c61ab6f3cac998fd4021f2684a01b91", size = 46326, upload-time = "2025-06-02T13:13:11.495Z" }, +] + +[[package]] +name = "markdownify" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, +] + [[package]] name = "mcp" -version = "1.22.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -999,9 +1602,59 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/a2/c5ec0ab38b35ade2ae49a90fada718fbc76811dc5aa1760414c6aaa6b08a/mcp-1.22.0.tar.gz", hash = "sha256:769b9ac90ed42134375b19e777a2858ca300f95f2e800982b3e2be62dfc0ba01", size = 471788, upload-time = "2025-11-20T20:11:28.095Z" } +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 = "mcp-atlassian" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "atlassian-python-api" }, + { name = "beautifulsoup4" }, + { name = "cachetools" }, + { name = "click" }, + { name = "fastmcp" }, + { name = "httpx" }, + { name = "keyring" }, + { name = "markdown" }, + { name = "markdown-to-confluence" }, + { name = "markdownify" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "requests", extra = ["socks"] }, + { name = "starlette" }, + { name = "thefuzz" }, + { name = "trio" }, + { name = "types-cachetools" }, + { name = "types-python-dateutil" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/f2/50f61d60e6a13c0e3177486eb174214b1dc79d9b514bc854ca4a2666d068/mcp_atlassian-0.13.0.tar.gz", hash = "sha256:c446e2f25dff0573232f1a303acb3bb15b110bc91ae0f71f406031eb1520fc42", size = 467415, upload-time = "2026-01-06T11:12:50.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/74/36dd0cf80bc8064684219841137cbb811feee16ece6b83f5f81451917d16/mcp_atlassian-0.13.0-py3-none-any.whl", hash = "sha256:bfbf3cf5c2d987eee6a10b03a1d09789f717def356684dab94021b6823d604cf", size = 180935, upload-time = "2026-01-06T11:12:49.192Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489, upload-time = "2025-11-20T20:11:26.542Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] @@ -1130,6 +1783,15 @@ 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 = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "openai" version = "2.8.1" @@ -1149,6 +1811,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.38.0" @@ -1231,6 +1905,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1240,6 +1926,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1249,6 +1944,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, +] + [[package]] name = "platformdirs" version = "4.5.0" @@ -1267,6 +1971,15 @@ 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 = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -1381,6 +2094,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -1413,7 +2167,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.11.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1421,106 +2175,79 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/54/ecab642b3bed45f7d5f59b38443dcb36ef50f85af192e6ece103dbfe9587/pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423", size = 788494, upload-time = "2025-10-04T10:40:41.338Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.33.2" 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] @@ -1537,6 +2264,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] +[[package]] +name = "pydocket" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/65/eb3b4f7afac80308d74bab2a668b31f074524ff6fbc664a685c6ed7c8885/pydocket-0.17.3.tar.gz", hash = "sha256:8922b4ca5f3f428e69b7695b9b5a313bbedc3ce35c74045cadcd89f7c0e6ac2d", size = 329828, upload-time = "2026-01-27T01:08:06.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/3b/29c69e4f88f5e5ea5e90e3cf93493cafb68bf9a2f625b916cc26ab1def89/pydocket-0.17.3-py3-none-any.whl", hash = "sha256:9ef2c6e855f52a3210acff300bcbcc45773d79295e2deddcc7aef7f67b2a5ba7", size = 91626, upload-time = "2026-01-27T01:08:05.085Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1560,6 +2308,37 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + [[package]] name = "pytest" version = "9.0.1" @@ -1603,6 +2382,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1612,6 +2403,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -1649,18 +2449,173 @@ wheels = [ { 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 = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + [[package]] name = "referencing" -version = "0.37.0" +version = "0.36.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] @@ -1678,6 +2633,50 @@ 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.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "14.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "rpds-py" version = "0.29.0" @@ -1824,6 +2823,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1833,6 +2863,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + [[package]] name = "sse-starlette" version = "3.0.3" @@ -1858,6 +2906,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "thefuzz" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -1919,6 +2979,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "types-cachetools" +version = "6.2.0.20251022" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/a8/f9bcc7f1be63af43ef0170a773e2d88817bcc7c9d8769f2228c802826efe/types_cachetools-6.2.0.20251022.tar.gz", hash = "sha256:f1d3c736f0f741e89ec10f0e1b0138625023e21eb33603a930c149e0318c0cef", size = 9608, upload-time = "2025-10-22T03:03:58.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/2d/8d821ed80f6c2c5b427f650bf4dc25b80676ed63d03388e4b637d2557107/types_cachetools-6.2.0.20251022-py3-none-any.whl", hash = "sha256:698eb17b8f16b661b90624708b6915f33dbac2d185db499ed57e4997e7962cad", size = 9341, upload-time = "2025-10-22T03:03:57.036Z" }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20251117" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/f3/d9a1bbba7b42b5558a3f9fe017d967f5338cf8108d35991d9b15fdea3e0d/types_html5lib-1.1.11.20251117.tar.gz", hash = "sha256:1a6a3ac5394aa12bf547fae5d5eff91dceec46b6d07c4367d9b39a37f42f201a", size = 18100, upload-time = "2025-11-17T03:08:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ab/f5606db367c1f57f7400d3cb3bead6665ee2509621439af1b29c35ef6f9e/types_html5lib-1.1.11.20251117-py3-none-any.whl", hash = "sha256:2a3fc935de788a4d2659f4535002a421e05bea5e172b649d33232e99d4272d08", size = 24302, upload-time = "2025-11-17T03:07:59.996Z" }, +] + +[[package]] +name = "types-lxml" +version = "2026.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cssselect" }, + { name = "types-html5lib" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/4a/06a169bd65a7570d107216200b61f4f81c0833d7d9c5410fd0166f2ac776/types_lxml-2026.1.1.tar.gz", hash = "sha256:b1066ab033bab6c046e4c9e6f0368ab5713fe0a2e30ffe8f92ff449e07662d2d", size = 159838, upload-time = "2025-12-31T18:13:41.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/95/d28c06c43eb4a9fe6fff630c9ceb820214b9f01e93f8297449a646a28b54/types_lxml-2026.1.1-py3-none-any.whl", hash = "sha256:b01dc6f6547713642ce3c44c77218501d7ae4a66a01b977d9df97825e8ec7f13", size = 98550, upload-time = "2025-12-31T18:13:39.834Z" }, +] + +[[package]] +name = "types-markdown" +version = "3.10.0.20251106" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e4/060f0dadd9b551cae77d6407f2bc84b168f918d90650454aff219c1b3ed2/types_markdown-3.10.0.20251106.tar.gz", hash = "sha256:12836f7fcbd7221db8baeb0d3a2f820b95050d0824bfa9665c67b4d144a1afa1", size = 19486, upload-time = "2025-11-06T03:06:44.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/58/f666ca9391f2a8bd33bb0b0797cde6ac3e764866708d5f8aec6fab215320/types_markdown-3.10.0.20251106-py3-none-any.whl", hash = "sha256:2c39512a573899b59efae07e247ba088a75b70e3415e81277692718f430afd7e", size = 25862, upload-time = "2025-11-06T03:06:43.082Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260124" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/41/4f8eb1ce08688a9e3e23709ed07089ccdeaf95b93745bfb768c6da71197d/types_python_dateutil-2.9.0.20260124.tar.gz", hash = "sha256:7d2db9f860820c30e5b8152bfe78dbdf795f7d1c6176057424e8b3fdd1f581af", size = 16596, upload-time = "2026-01-24T03:18:42.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/c2/aa5e3f4103cc8b1dcf92432415dde75d70021d634ecfd95b2e913cf43e17/types_python_dateutil-2.9.0.20260124-py3-none-any.whl", hash = "sha256:f802977ae08bf2260142e7ca1ab9d4403772a254409f7bbdf652229997124951", size = 18266, upload-time = "2026-01-24T03:18:42.155Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20251108" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/d6/75e381959a2706644f02f7527d264de3216cf6ed333f98eff95954d78e07/types_webencodings-0.5.0.20251108.tar.gz", hash = "sha256:2378e2ceccced3d41bb5e21387586e7b5305e11519fc6b0659c629f23b2e5de4", size = 7470, upload-time = "2025-11-08T02:56:00.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/4e/8fcf33e193ce4af03c19d0e08483cf5f0838e883f800909c6bc61cb361be/types_webencodings-0.5.0.20251108-py3-none-any.whl", hash = "sha256:e21f81ff750795faffddaffd70a3d8bfff77d006f22c27e393eb7812586249d8", size = 8715, upload-time = "2025-11-08T02:55:59.456Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1962,6 +3138,201 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "wrapt" version = "1.17.3"