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"