diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..987e3cda45e5e92a184a0cb170c0ee46b2b82bd2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..31a1dfa308744e32ce24cb48b9aec01de0fcf527 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.cadence +.env +.idea +.mypy_cache/ +archive/ +cache +logs + +# Test-generated files +test_cache/ +.coverage +htmlcov/ +.pytest_cache/ +.ruff_cache +assets +static diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000000000000000000000000000000000..e4fba2183587225f216eeada4c78dfab6b2e65f5 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d9c086fac2526e706515e8fb95c84a7cc2cbce9c --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +--- +title: ShallowCodeResearch +emoji: πŸ“‰ +colorFrom: blue +colorTo: pink +sdk: gradio +sdk_version: 5.33.0 +app_file: app.py +pinned: false +short_description: Coding research assistant that generates code and tests it +tags: + - mcp + - multi-agent + - research + - code-generation + - ai-assistant + - gradio + - python + - web-search + - llm + - modal +python_version: "3.12" +--- +--- + +# MCP Hub - Multi-Agent AI Research & Code Assistant + +πŸš€ **Advanced multi-agent system for AI-powered research and code generation** + +## What is MCP Hub? + +MCP Hub is a sophisticated multi-agent research and code assistant built using Gradio's Model Context Protocol (MCP) server functionality. It orchestrates specialized AI agents to provide comprehensive research capabilities and generate executable Python code. + +## ✨ Key Features + +- 🧠 **Multi-Agent Architecture**: Specialized agents working in orchestrated workflows +- πŸ” **Intelligent Research**: Web search with automatic summarization and citation formatting +- πŸ’» **Code Generation**: Context-aware Python code creation with secure execution +- πŸ”— **MCP Server**: Built-in MCP server for seamless agent communication +- 🎯 **Multiple LLM Support**: Compatible with Nebius, OpenAI, Anthropic, and HuggingFace +- πŸ›‘οΈ **Secure Execution**: Modal sandbox environment for safe code execution +- πŸ“Š **Performance Monitoring**: Advanced metrics collection and health monitoring + +## πŸš€ Quick Start + +1. **Configure your environment** by setting up API keys in the Settings tab +2. **Choose your LLM provider** (Nebius recommended for best performance) +3. **Input your research query** in the Orchestrator Flow tab +4. **Watch the magic happen** as agents collaborate to research and generate code + +## πŸ—οΈ Architecture + +### Core Agents + +- **Question Enhancer**: Breaks down complex queries into focused sub-questions +- **Web Search Agent**: Performs targeted searches using Tavily API +- **LLM Processor**: Handles text processing, summarization, and analysis +- **Citation Formatter**: Manages academic citation formatting (APA style) +- **Code Generator**: Creates contextually-aware Python code +- **Code Runner**: Executes code in secure Modal sandboxes +- **Orchestrator**: Coordinates the complete workflow + +### Workflow Example + +``` +User Query: "Create Python code to analyze Twitter sentiment" + ↓ +Question Enhancement: Split into focused sub-questions + ↓ +Web Research: Search for Twitter APIs, sentiment libraries, examples + ↓ +Context Integration: Combine research into comprehensive context + ↓ +Code Generation: Create executable Python script + ↓ +Secure Execution: Run code in Modal sandbox + ↓ +Results: Code + output + research summary + citations +``` + +## πŸ› οΈ Setup Requirements + +### Required API Keys + +- **LLM Provider** (choose one): + - Nebius API (recommended) + - OpenAI API + - Anthropic API + - HuggingFace Inference API +- **Tavily API** (for web search) +- **Modal Account** (for code execution) + +### Environment Configuration + +Set these environment variables or configure in the app: + +```bash +LLM_PROVIDER=nebius # Your chosen provider +NEBIUS_API_KEY=your_key_here +TAVILY_API_KEY=your_key_here +# Modal setup handled automatically +``` + +## 🎯 Use Cases + +### Research & Development +- **Academic Research**: Automated literature review and citation management +- **Technical Documentation**: Generate comprehensive guides with current information +- **Market Analysis**: Research trends and generate analytical reports + +### Code Generation +- **Prototype Development**: Rapidly create functional code based on requirements +- **API Integration**: Generate code for working with various APIs and services +- **Data Analysis**: Create scripts for data processing and visualization + +### Learning & Education +- **Code Examples**: Generate educational code samples with explanations +- **Concept Exploration**: Research and understand complex programming concepts +- **Best Practices**: Learn current industry standards and methodologies + +## πŸ”§ Advanced Features + +### Performance Monitoring +- Real-time metrics collection +- Response time tracking +- Success rate monitoring +- Resource usage analytics + +### Intelligent Caching +- Reduces redundant API calls +- Improves response times +- Configurable TTL settings + +### Fault Tolerance +- Circuit breaker protection +- Rate limiting management +- Graceful error handling +- Automatic retry mechanisms + +### Sandbox Pool Management +- Pre-warmed execution environments +- Optimized performance +- Resource pooling +- Automatic scaling + +## πŸ“± Interface Tabs + +1. **Orchestrator Flow**: Complete end-to-end workflow +2. **Individual Agents**: Access each agent separately for specific tasks +3. **Advanced Features**: System monitoring and performance analytics + +## 🀝 MCP Integration + +This application demonstrates advanced MCP (Model Context Protocol) implementation: + +- **Server Architecture**: Full MCP server with schema generation +- **Function Registry**: Proper MCP function definitions with typing +- **Multi-Agent Communication**: Structured data flow between agents +- **Error Handling**: Robust error management across agent interactions + +## πŸ“Š Performance + +- **Response Times**: Optimized for sub-second agent responses +- **Scalability**: Handles concurrent requests efficiently +- **Reliability**: Built-in fault tolerance and monitoring +- **Resource Management**: Intelligent caching and pooling + +## πŸ” Technical Details + +- **Python**: 3.12+ required +- **Framework**: Gradio with MCP server capabilities +- **Execution**: Modal for secure sandboxed code execution +- **Search**: Tavily API for real-time web research +- **Monitoring**: Comprehensive performance and health tracking + +--- + +**Ready to experience the future of AI-assisted research and development?** + +Start by configuring your API keys and dive into the world of multi-agent AI collaboration! πŸš€ diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..f0e3d0d3a738baf101bd8c6c343e908633b4bb10 --- /dev/null +++ b/app.py @@ -0,0 +1,2392 @@ +""" +Enhanced MCP Hub - Single Unified Version with Advanced Features. + +This module provides a comprehensive MCP (Model Context Protocol) Hub that integrates +multiple AI agents for research, code generation, and execution. It includes web search, +question enhancement, LLM processing, code generation, and secure code execution capabilities. + +The hub is designed to be used as both a Gradio web interface and as an MCP server, +providing a unified API for AI-assisted development workflows. +""" +import gradio as gr +import modal +import textwrap +import base64 +import marshal +import types +import time +import asyncio +import aiohttp +import ast +import json +from typing import Dict, Any, List +from functools import wraps +from contextlib import asynccontextmanager + +# Import our custom modules +from mcp_hub.config import api_config, model_config, app_config +from mcp_hub.exceptions import APIError, ValidationError, CodeGenerationError, CodeExecutionError +from mcp_hub.utils import ( + validate_non_empty_string, extract_json_from_text, + extract_urls_from_text, make_llm_completion, + create_apa_citation +) +from mcp_hub.logging_config import logger +from tavily import TavilyClient + +# Import advanced features with graceful fallback +ADVANCED_FEATURES_AVAILABLE = False +try: + from mcp_hub.performance_monitoring import metrics_collector, track_performance, track_api_call + from mcp_hub.cache_utils import cached + from mcp_hub.reliability_utils import rate_limited, circuit_protected + from mcp_hub.health_monitoring import health_monitor + ADVANCED_FEATURES_AVAILABLE = True + logger.info("Advanced features loaded successfully") + +except ImportError as e: + logger.info(f"Advanced features not available: {e}") + logger.info("Running with basic features only") + + # Create dummy decorators for backward compatibility + def track_performance(operation_name: str = None): + def decorator(func): + return func + return decorator + + def track_api_call(service_name: str): + def decorator(func): + return func + return decorator + + def rate_limited(service: str = "default", timeout: float = 10.0): + def decorator(func): + return func + return decorator + + def circuit_protected(service: str = "default"): + def decorator(func): + return func + return decorator + + def cached(ttl: int = 300): + def decorator(func): + return func + return decorator + +# Performance tracking wrapper +def with_performance_tracking(operation_name: str): + """ + Add performance tracking and metrics collection to any function (sync or async). + + This decorator wraps both synchronous and asynchronous functions to collect + execution time, success/failure metrics, and error counts. It integrates with + the advanced monitoring system when available. + + Args: + operation_name (str): The name of the operation to track in metrics + + Returns: + function: A decorator function that can wrap sync or async functions + """ + def decorator(func): + if asyncio.iscoroutinefunction(func): + @wraps(func) + async def async_wrapper(*args, **kwargs): + start_time = time.time() + try: + result = await func(*args, **kwargs) + success = True + error = None + except Exception as e: + success = False + error = str(e) + raise + finally: + duration = time.time() - start_time + if ADVANCED_FEATURES_AVAILABLE: + metrics_collector.record_metric(f"{operation_name}_duration", duration, + {"success": str(success), "operation": operation_name}) + if not success: + metrics_collector.increment_counter(f"{operation_name}_errors", 1, + {"operation": operation_name, "error": error}) + logger.info(f"Operation {operation_name} completed in {duration:.2f}s (success: {success})") + return result + return async_wrapper + else: + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + try: + result = func(*args, **kwargs) + success = True + error = None + except Exception as e: + success = False + error = str(e) + raise + finally: + duration = time.time() - start_time + if ADVANCED_FEATURES_AVAILABLE: + metrics_collector.record_metric(f"{operation_name}_duration", duration, + {"success": str(success), "operation": operation_name}) + if not success: + metrics_collector.increment_counter(f"{operation_name}_errors", 1, + {"operation": operation_name, "error": error}) + logger.info(f"Operation {operation_name} completed in {duration:.2f}s (success: {success})") + return result + return wrapper + return decorator + +class QuestionEnhancerAgent: + """ + Agent responsible for enhancing questions into sub-questions for research. + + This agent takes a single user query and intelligently breaks it down into + multiple distinct, non-overlapping sub-questions that explore different + technical angles of the original request. It uses LLM models to enhance + question comprehension and research depth. """ + + @with_performance_tracking("question_enhancement") + @rate_limited("nebius") + @circuit_protected("nebius") + @cached(ttl=300) # Cache for 5 minutes + def enhance_question(self, user_request: str, num_questions: int) -> Dict[str, Any]: + """ + Split a single user query into multiple distinct sub-questions for enhanced research. + + Takes a user's original request and uses LLM processing to break it down into + separate sub-questions that explore different technical angles. This enables + more comprehensive research and analysis of complex topics. + + Args: + user_request (str): The original user query to be enhanced and split + num_questions (int): The number of sub-questions to generate + + Returns: + Dict[str, Any]: A dictionary containing the generated sub-questions array + or error information if processing fails + """ + try: + validate_non_empty_string(user_request, "User request") + logger.info(f"Enhancing question: {user_request[:100]}...") + + prompt_text = f""" + You are an AI assistant specialised in Python programming that must break a single user query into {num_questions} distinct, non-overlapping sub-questions. + Each sub-question should explore a different technical angle of the original request. + Output must be valid JSON with a top-level key "sub_questions" whose value is an array of stringsβ€”no extra keys, no extra prose. + + User Request: "{user_request}" + + Respond with exactly: + {{ + "sub_questions": [ + "First enhanced sub-question …", + "Second enhanced sub-question …", + ........ more added as necessary + ] + }} + """ + + messages = [{"role": "user", "content": prompt_text}] + response_format = { + "type": "json_object", + "object": { + "sub_questions": { + "type": "array", + "items": {"type": "string"}, + } + }, + } + + logger.info( + "The LLM provider is: %s and the model is: %s", + api_config.llm_provider, + model_config.get_model_for_provider("question_enhancer", api_config.llm_provider) + ) + + raw_output = make_llm_completion( + model=model_config.get_model_for_provider("question_enhancer", api_config.llm_provider), + messages=messages, + temperature=0.7, + response_format=response_format + ) + + parsed = extract_json_from_text(raw_output) + + if "sub_questions" not in parsed: + raise ValidationError("JSON does not contain a 'sub_questions' key.") + + sub_questions = parsed["sub_questions"] + if not isinstance(sub_questions, list) or not all(isinstance(q, str) for q in sub_questions): + raise ValidationError("Expected 'sub_questions' to be a list of strings.") + + logger.info(f"Successfully generated {len(sub_questions)} sub-questions") + return {"sub_questions": sub_questions} + + except (ValidationError, APIError) as e: + logger.error(f"Question enhancement failed: {str(e)}") + return {"error": str(e), "sub_questions": []} + except Exception as e: + logger.error(f"Unexpected error in question enhancement: {str(e)}") + return {"error": f"Unexpected error: {str(e)}", "sub_questions": []} + +class WebSearchAgent: + """ + Agent responsible for performing web searches using the Tavily API. + + This agent handles web search operations to gather information from the internet. + It provides both synchronous and asynchronous search capabilities with configurable + result limits and search depth. Results include summaries, URLs, and content snippets. + """ + + def __init__(self): + if not api_config.tavily_api_key: + raise APIError("Tavily", "API key not configured") + self.client = TavilyClient(api_key=api_config.tavily_api_key) + + @with_performance_tracking("web_search") + @rate_limited("tavily") + @circuit_protected("tavily") + @cached(ttl=600) # Cache for 10 minutes + def search(self, query: str) -> Dict[str, Any]: + """ + Perform a web search using the Tavily API to gather internet information. + + Executes a synchronous web search with the specified query and returns + structured results including search summaries, URLs, and content snippets. + Results are cached for performance optimization. + + Args: + query (str): The search query string to look up on the web + + Returns: + Dict[str, Any]: A dictionary containing search results, summaries, and metadata + or error information if the search fails + """ + try: + validate_non_empty_string(query, "Search query") + logger.info(f"Performing web search: {query}") + + response = self.client.search( + query=query, + search_depth="basic", + max_results=app_config.max_search_results, + include_answer=True + ) + + logger.info(f"Search completed, found {len(response.get('results', []))} results") + return { + "query": response.get("query", query), + "tavily_answer": response.get("answer"), + "results": response.get("results", []), + "data_source": "Tavily Search API", + } + + except ValidationError as e: + logger.error(f"Web search validation failed: {str(e)}") + return {"error": str(e), "query": query, "results": []} + except Exception as e: + logger.error(f"Web search failed: {str(e)}") + return {"error": f"Tavily API Error: {str(e)}", "query": query, "results": []} + + @with_performance_tracking("async_web_search") + @rate_limited("tavily") + @circuit_protected("tavily") + async def search_async(self, query: str) -> Dict[str, Any]: + """ + Perform an asynchronous web search using aiohttp for better performance. + + Executes an async web search with the specified query using direct HTTP calls + to the Tavily API. Falls back to synchronous search if async fails. + Provides better performance for concurrent operations. + + Args: + query (str): The search query string to look up on the web + + Returns: + Dict[str, Any]: A dictionary containing search results, summaries, and metadata + or falls back to synchronous search on error + """ + try: + validate_non_empty_string(query, "Search query") + logger.info(f"Performing async web search: {query}") + + # Use async HTTP client for better performance + async with aiohttp.ClientSession() as session: + headers = { + 'Authorization': f'Bearer {api_config.tavily_api_key}', + 'Content-Type': 'application/json' + } + + payload = { + 'query': query, + 'search_depth': 'basic', + 'max_results': app_config.max_search_results, + 'include_answer': True + } + + async with session.post( + 'https://api.tavily.com/search', + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + logger.info(f"Async search completed, found {len(data.get('results', []))} results") + return { + "query": data.get("query", query), + "tavily_answer": data.get("answer"), + "results": data.get("results", []), + "data_source": "Tavily Search API (Async)", + } + else: + error_text = await response.text() + raise Exception(f"HTTP {response.status}: {error_text}") + + except ValidationError as e: + logger.error(f"Async web search validation failed: {str(e)}") + return {"error": str(e), "query": query, "results": []} + except Exception as e: + logger.error(f"Async web search failed: {str(e)}") + # Fallback to sync version on error + logger.info("Falling back to synchronous search") + return self.search(query) + +class LLMProcessorAgent: + """ + Agent responsible for processing text using Large Language Models for various tasks. + + This agent handles text processing operations including summarization, reasoning, + and keyword extraction using configured LLM providers. It supports both synchronous + and asynchronous processing with configurable temperature and response formats. """ + + @with_performance_tracking("llm_processing") + @rate_limited("nebius") + @circuit_protected("nebius") + def process(self, text_input: str, task: str, context: str = None) -> Dict[str, Any]: + """ + Process text using LLM for summarization, reasoning, or keyword extraction. + + Applies the configured LLM model to process the input text according to the + specified task type. Supports summarization for condensing content, reasoning + for analytical tasks, and keyword extraction for identifying key terms. + + Args: + text_input (str): The input text to be processed by the LLM + task (str): The processing task ('summarize', 'reason', or 'extract_keywords') + context (str, optional): Additional context to guide the processing + + Returns: + Dict[str, Any]: A dictionary containing the processed output and metadata + or error information if processing fails + """ + try: + validate_non_empty_string(text_input, "Input text") + validate_non_empty_string(task, "Task") + logger.info(f"Processing text with task: {task}") + + task_lower = task.lower() + if task_lower not in ["reason", "summarize", "extract_keywords"]: + raise ValidationError( + f"Unsupported LLM task: {task}. Choose 'summarize', 'reason', or 'extract_keywords'." + ) + + prompt_text = self._build_prompt(text_input, task_lower, context) + messages = [{"role": "user", "content": prompt_text}] + + logger.info(f"LLM provider is: {api_config.llm_provider}, model used: {model_config.get_model_for_provider('llm_processor', api_config.llm_provider)}") + + output_text = make_llm_completion( + model=model_config.get_model_for_provider("llm_processor", api_config.llm_provider), + messages=messages, + temperature=app_config.llm_temperature + ) + + logger.info(f"LLM processing completed for task: {task}") + return { + "input_text": text_input, + "task": task, + "provided_context": context, + "llm_processed_output": output_text, + "llm_model_used": model_config.get_model_for_provider("llm_processor", api_config.llm_provider), + } + + except (ValidationError, APIError) as e: + logger.error(f"LLM processing failed: {str(e)}") + return {"error": str(e), "input_text": text_input, "processed_output": None} + except Exception as e: + logger.error(f"Unexpected error in LLM processing: {str(e)}") + return {"error": f"Unexpected error: {str(e)}", "input_text": text_input, "processed_output": None} + + @with_performance_tracking("async_llm_processing") + @rate_limited("nebius") + @circuit_protected("nebius") + async def async_process(self, text_input: str, task: str, context: str = None) -> Dict[str, Any]: + """ + Process text using async LLM for summarization, reasoning, or keyword extraction. + + Asynchronous version of the text processing function that provides better + performance for concurrent operations. Uses async LLM completion calls + for improved throughput when processing multiple texts simultaneously. + + Args: + text_input (str): The input text to be processed by the LLM + task (str): The processing task ('summarize', 'reason', or 'extract_keywords') + context (str, optional): Additional context to guide the processing + + Returns: + Dict[str, Any]: A dictionary containing the processed output and metadata + or error information if processing fails + """ + try: + validate_non_empty_string(text_input, "Input text") + validate_non_empty_string(task, "Task") + logger.info(f"Processing text async with task: {task}") + + task_lower = task.lower() + if task_lower not in ["reason", "summarize", "extract_keywords"]: + raise ValidationError( + f"Unsupported LLM task: {task}. Choose 'summarize', 'reason', or 'extract_keywords'." + ) + + prompt_text = self._build_prompt(text_input, task_lower, context) + messages = [{"role": "user", "content": prompt_text}] + + logger.info(f"LLM provider is: {api_config.llm_provider}, model used: {model_config.get_model_for_provider('llm_processor', api_config.llm_provider)}") + + from mcp_hub.utils import make_async_llm_completion + output_text = await make_async_llm_completion( + model=model_config.get_model_for_provider("llm_processor", api_config.llm_provider), + messages=messages, + temperature=app_config.llm_temperature + ) + + logger.info(f"Async LLM processing completed for task: {task}") + return { + "input_text": text_input, + "task": task, + "provided_context": context, + "llm_processed_output": output_text, + "llm_model_used": model_config.get_model_for_provider("llm_processor", api_config.llm_provider), + } + + except (ValidationError, APIError) as e: + logger.error(f"Async LLM processing failed: {str(e)}") + return {"error": str(e), "input_text": text_input, "processed_output": None} + except Exception as e: + logger.error(f"Unexpected error in async LLM processing: {str(e)}") + return {"error": f"Unexpected error: {str(e)}", "input_text": text_input, "processed_output": None} + + def _build_prompt(self, text_input: str, task: str, context: str = None) -> str: + """Build the appropriate prompt based on the task.""" + prompts = { + "reason": f"Analyze this text and provide detailed reasoning (less than 250):\n\n{text_input} with this context {context if context else ''} for {task}", + "summarize": f"Summarize in detail (less than 250):\n\n{text_input} with this context {context if context else ''} for {task}", + "extract_keywords": f"Extract key terms/entities (comma-separated) from:\n\n{text_input}" + } + + prompt = prompts[task] + + if context: + context_additions = { + "reason": f"\n\nAdditional context: {context}", + "summarize": f"\n\nKeep in mind this context: {context}", + "extract_keywords": f"\n\nFocus on this context: {context}" + } + prompt += context_additions[task] + + task_endings = { + "reason": "\n\nReasoning:", + "summarize": "\n\nSummary:", + "extract_keywords": "\n\nKeywords:" + } + prompt += task_endings[task] + + return prompt + +class CitationFormatterAgent: + """ + Agent responsible for formatting citations from text content. + + This agent extracts URLs from text blocks and produces properly formatted + APA-style citations. It handles the automated creation of academic references + from web sources found in research content. + """ + + @with_performance_tracking("citation_formatting") + def format_citations(self, text_block: str) -> Dict[str, Any]: + """ + Extract URLs from text and produce APA-style citations. + + Analyzes the provided text block to identify URLs and automatically + generates properly formatted academic citations following APA style + guidelines for web sources. + + Args: + text_block (str): The text content containing URLs to be cited + + Returns: + Dict[str, Any]: A dictionary containing formatted citations array + or error information if extraction fails + """ + try: + validate_non_empty_string(text_block, "Text block") + logger.info("Formatting citations from text block") + + urls = extract_urls_from_text(text_block) + if not urls: + return {"error": "No URLs found to cite.", "formatted_citations": []} + + citations = [] + for url in urls: + citation = create_apa_citation(url) + citations.append(citation) + + logger.info(f"Successfully formatted {len(citations)} citations") + return {"formatted_citations": citations, "error": None} + + except ValidationError as e: + logger.error(f"Citation formatting validation failed: {str(e)}") + return {"error": str(e), "formatted_citations": []} + except Exception as e: + logger.error(f"Citation formatting failed: {str(e)}") + return {"error": f"Unexpected error: {str(e)}", "formatted_citations": []} + +class CodeGeneratorAgent: + """ + Agent responsible for generating Python code based on user requests and context. + + This agent generates secure Python code using LLM models with built-in security + checks and validation. It enforces restrictions on dangerous function calls and + modules, ensures code compilation, and provides iterative error correction. + """ + + # List of disallowed function calls for security + DISALLOWED_CALLS = { + "input", "eval", "exec", "compile", "__import__", "open", + "file", "raw_input", "execfile", "reload", "quit", "exit" + } + + def _uses_disallowed_calls(self, code_str: str) -> tuple[bool, list[str]]: + """Check if code uses disallowed function calls.""" + violations = [] + try: + tree = ast.parse(code_str) + for node in ast.walk(tree): + if isinstance(node, ast.Call): + if isinstance(node.func, ast.Name) and node.func.id in self.DISALLOWED_CALLS: + violations.append(node.func.id) + elif isinstance(node, ast.Import): + for alias in node.names: + if alias.name in ["os", "subprocess", "sys"]: + violations.append(f"import {alias.name}") + elif isinstance(node, ast.ImportFrom): + if node.module in ["os", "subprocess", "sys"]: + violations.append(f"from {node.module} import ...") + except SyntaxError: + # Don't treat syntax errors as security violations - let them be handled separately + return False, [] + + return len(violations) > 0, violations + + def _make_prompt(self, user_req: str, ctx: str, prev_err: str = "") -> str: + """Create a prompt for code generation with error feedback.""" + disallowed_list = ", ".join(self.DISALLOWED_CALLS) + prev_error_text = "" + if prev_err: + prev_error_text = f"Previous attempt failed:\n{prev_err}\nFix it." + + return f""" + You are an expert Python developer. **Rules**: + - Never use these functions: {disallowed_list} + - Never import os, subprocess, or sys modules + - After defining functions/classes, call them and print the result. + - Always include print statements to show output + {prev_error_text} + + USER REQUEST: + \"\"\"{user_req}\"\"\" + + CONTEXT: + \"\"\"{ctx}\"\"\" + + Provide only valid Python code that can be executed safely. + + Provide only the Python code and never under any circumstance include any + explanations in your response. **Do not include back ticks or the word python + and dont include input fields** + + for example, + + import requests + response = requests.get("https://api.example.com/data") + print(response.json()) + + or + + def add_numbers(a, b): + return a + b + result = add_numbers(5, 10) + print(result) + + NEVER include input() or Never use input(), even in disguised forms like raw_input() + + ALWAYS return valid Python code that can be executed without errors. The code returned should be + a function or class depending on the complexity. For simple requests, return a function, + and for more complex requests, return a class with methods that can be called. + + After the creation of classes or functions, classes should be instantiated or functions should be called + to demonstrate their usage. The final step is include the print function of the result of the class and/or function. + + for example + + class DataFetcher: + def __init__(self, url): + self.url = url + def fetch_data(self): + response = requests.get(self.url) + return response.json() + fetcher = DataFetcher("https://api.example.com/data") + data = fetcher.fetch_data() + print(data) + + if the code requires and data manipulation etc, generate the code to test the code and print the result. + + for example; + def process_data(data): + # Perform some data manipulation + return data * 2 + data = 5 + + or + + For example, to get the mean of a column in a pandas DataFrame: + + import pandas as pd + + def get_mean_of_column(df, column_name): + return df[column_name].mean() + + df = pd.DataFrame({{'A': [1, 2, 3], 'B': [4, 5, 6]}}) + mean_value = get_mean_of_column(df, 'A') + print(mean_value) + + # If you want to pretty-print the DataFrame: + import json + print(json.dumps(df.to_dict(), indent=2)) + + Never wrap dictionaries or lists in f-strings in print statements (e.g., avoid print(f"{{my_dict}}")). + + To print a dict or list, use print(my_dict) or, if you want pretty output, use the json module: + + import json + print(json.dumps(my_dict, indent=2)) + If you need to include a variable in a string, only use f-strings with simple values, not dicts or lists. + + + + Never wrap dictionaries or lists in f-strings in print statements, like this: + + # ❌ BAD EXAMPLE β€” NEVER DO THIS: + my_dict = {{'A': [1,2,3], 'B': [4,5,6]}} + print(f"{{my_dict}}") + + # ❌ BAD EXAMPLE β€” NEVER DO THIS: + my_list = [1, 2, 3] + print(f"{{my_list}}") + + # βœ… GOOD EXAMPLES β€” ALWAYS DO THIS INSTEAD: + print(my_dict) + print(my_list) + + # βœ… Or, for pretty output, do: + import json + print(json.dumps(my_dict, indent=2)) + + If you need to include a variable in a string, only use f-strings with simple scalar values, not dicts or lists. For example: + + # βœ… Good f-string with a simple value: + mean = 3.5 + print(f"The mean is {{mean}}") + + # ❌ Bad f-string with a dict: + print(f"The data is {{my_dict}}") # <-- NEVER DO THIS + + # βœ… Good way to show a dict: + print("The data is:", my_dict) + + ### **Summary** + + - Repeat the "NEVER wrap dicts/lists in f-strings" rule. + - Give both *bad* and *good* code examples. + - Use all-caps or bold/emoji to make "NEVER" and "ALWAYS" pop out. + - Finish the prompt by *repeating* the most important style rule. + + """ + + @with_performance_tracking("code_generation") + @rate_limited("nebius") + @circuit_protected("nebius") + def generate_code( + self, user_request: str, grounded_context: str + ) -> tuple[Dict[str, Any], str]: + """ + Generate Python code based on user request and grounded context with enhanced security. + + Creates safe, executable Python code using LLM models with built-in security + validation. Includes iterative error correction, syntax checking, and + security violation detection to ensure safe code generation. + + Args: + user_request (str): The user's request describing what code to generate + grounded_context (str): Contextual information to inform code generation + + Returns: + tuple[Dict[str, Any], str]: A tuple containing the generation result dictionary + and the raw generated code string + """ + try: + validate_non_empty_string(user_request, "User request") + logger.info("Generating Python code with security checks") + + prev_error = "" + + for attempt in range(1, app_config.max_code_generation_attempts + 1): + try: + logger.info(f"Code generation attempt {attempt}") + + prompt_text = self._make_prompt(user_request, grounded_context, prev_error) + messages = [{"role": "user", "content": prompt_text}] + + logger.info(f"LLM provider is: {api_config.llm_provider}, model used: {model_config.get_model_for_provider('code_generator', api_config.llm_provider)}") + + raw_output = make_llm_completion( + model=model_config.get_model_for_provider("code_generator", api_config.llm_provider), + messages=messages, + temperature=app_config.code_gen_temperature, + ) + logger.info(f"Generated code (attempt {attempt}):\n{raw_output}\n") + + # First, validate that the code compiles (syntax check) + try: + code_compiled = compile(raw_output, "", "exec") + except SyntaxError as syntax_err: + prev_error = f"Syntax error: {str(syntax_err)}" + logger.warning(f"Generated code syntax error (attempt {attempt}): {syntax_err}") + if attempt == app_config.max_code_generation_attempts: + raise CodeGenerationError( + f"Failed to generate valid Python syntax after {attempt} attempts" + ) + continue + + # Then security check: look for disallowed calls (only if syntax is valid) + has_violations, violations = self._uses_disallowed_calls(raw_output) + if has_violations: + prev_error = f"Security violation - used disallowed functions: {', '.join(violations)}" + logger.warning(f"Security violation in attempt {attempt}: {violations}") + if attempt == app_config.max_code_generation_attempts: + raise CodeGenerationError(f"Code contains security violations: {violations}") + continue + + logger.info(f"The generated code is as follows: \n\n{raw_output}\n") + logger.info("Code generation successful with security checks passed") + + return {"status": "success", "generated_code": code_compiled, "code": code_compiled}, raw_output + + except SyntaxError as e: + prev_error = f"Syntax error: {str(e)}" + logger.warning(f"Generated code syntax error (attempt {attempt}): {e}") + if attempt == app_config.max_code_generation_attempts: + raise CodeGenerationError( + f"Failed to generate valid Python after {attempt} attempts" + ) + continue + + except APIError as e: + raise CodeGenerationError(f"Unexpected API error: {e}") from e + + except Exception as e: + prev_error = f"Unexpected error: {str(e)}" + logger.error(f"Code generation error (attempt {attempt}): {e}") + if attempt == app_config.max_code_generation_attempts: + raise CodeGenerationError(f"Unexpected error: {e}") + continue + + raise CodeGenerationError("No valid code produced after all attempts") + except (ValidationError, APIError, CodeGenerationError) as e: + logger.error("Code generation failed: %s", e) + return {"error": str(e), "generated_code": ""}, "" + + except Exception as e: + logger.error("Unexpected error in code generation: %s", e) + return {"error": f"Unexpected error: {e}", "generated_code": ""}, "" + + + def _get_enhanced_image(self): + """Get Modal image with enhanced security and performance packages.""" + return ( + modal.Image.debian_slim(python_version="3.12") + .pip_install([ + "numpy", "pandas", "matplotlib", "seaborn", "plotly", + "requests", "beautifulsoup4", "lxml", "scipy", "scikit-learn", + "pillow", "opencv-python-headless", "wordcloud", "textblob" + ]) + .apt_install(["curl", "wget", "git"]) + .env({"PYTHONUNBUFFERED": "1", "PYTHONDONTWRITEBYTECODE": "1"}) + .run_commands([ + "python -m pip install --upgrade pip", + "pip install --no-cache-dir jupyter ipython" + ]) + ) + +class CodeRunnerAgent: + """ + Agent responsible for executing code in Modal sandbox with enhanced security. + + This agent provides secure code execution in isolated Modal sandbox environments + with warm sandbox pools for performance optimization. It includes safety shims, + package management, and both synchronous and asynchronous execution capabilities. + """ + + def __init__(self): + self.app = modal.App.lookup(app_config.modal_app_name, create_if_missing=True) + # Create enhanced image with common packages for better performance + self.image = self._create_enhanced_image() + # Initialize warm sandbox pool + self.sandbox_pool = None + self._pool_initialized = False + + def _create_enhanced_image(self): + """Create a lean Modal image with only essential packages pre-installed.""" + # Only include truly essential packages in the base image to reduce cold start time + essential_packages = [ + "numpy", + "pandas", + "matplotlib", + "requests", + "scikit-learn", + ] + + try: + return ( + modal.Image.debian_slim() + .pip_install(*essential_packages) + .apt_install(["curl", "wget", "git"]) + .env({"PYTHONUNBUFFERED": "1", "PYTHONDONTWRITEBYTECODE": "1"}) + ) + except Exception as e: + logger.warning(f"Failed to create enhanced image, using basic: {e}") + return modal.Image.debian_slim() + + async def _ensure_pool_initialized(self): + """Ensure the sandbox pool is initialized (lazy initialization).""" + if not self._pool_initialized: + from mcp_hub.sandbox_pool import WarmSandboxPool + self.sandbox_pool = WarmSandboxPool( + app=self.app, + image=self.image, + pool_size=5, # Increased from 3 to reduce cold starts + max_age_seconds=600, # Increased from 300 (10 minutes) + max_uses_per_sandbox=10 + ) + await self.sandbox_pool.start() + self._pool_initialized = True + logger.info("Warm sandbox pool initialized") + + async def get_pool_stats(self): + """Get sandbox pool statistics.""" + if self.sandbox_pool: + return self.sandbox_pool.get_stats() + return {"error": "Pool not initialized"} + + @asynccontextmanager + async def _sandbox_context(self, **kwargs): + """Context manager for safe sandbox lifecycle management.""" + sb = None + try: + sb = modal.Sandbox.create( + app=self.app, + image=self.image, + cpu=1.0, + memory=512, # MB + timeout=30, # seconds + **kwargs + ) + yield sb + except Exception as e: + logger.error(f"Sandbox creation failed: {e}") + raise CodeExecutionError(f"Failed to create sandbox: {e}") + finally: + if sb: + try: + sb.terminate() + except Exception as e: + logger.warning(f"Failed to terminate sandbox: {e}") + + def _add_safety_shim(self, code: str) -> str: + """Return code wrapped in the security shim, for file-based execution.""" + try: + safety_shim = f""" +import sys +import types +import functools +import builtins +import marshal +import traceback + +RESTRICTED_BUILTINS = {{ + 'open', 'input', 'eval', 'compile', '__import__', + 'getattr', 'setattr', 'delattr', 'hasattr', 'globals', 'locals', + 'pty', 'subprocess', 'socket', 'threading', 'ssl', 'email', 'smtpd' +}} + +if isinstance(__builtins__, dict): + _original_builtins = __builtins__.copy() +else: + _original_builtins = __builtins__.__dict__.copy() + +_safe_builtins = {{k: v for k, v in _original_builtins.items() if k not in RESTRICTED_BUILTINS}} +_safe_builtins['print'] = print + +def safe_exec(code_obj, globals_dict=None, locals_dict=None): + if not isinstance(code_obj, types.CodeType): + raise TypeError("safe_exec only accepts a compiled code object") + if globals_dict is None: + globals_dict = {{"__builtins__": types.MappingProxyType(_safe_builtins)}} + return _original_builtins['exec'](code_obj, globals_dict, locals_dict) + +_safe_builtins['exec'] = safe_exec + +def safe_import(name, *args, **kwargs): + ALLOWED_MODULES = ( + set(sys.stdlib_module_names) + .difference(RESTRICTED_BUILTINS) + .union({{ + "aiokafka", "altair", "anthropic", "apache-airflow", "apsw", "bokeh", "black", "bottle", "catboost", "click", + "confluent-kafka", "cryptography", "cupy", "dask", "dash", "datasets", "dagster", "django", "distributed", "duckdb", + "duckdb-engine", "elasticsearch", "evidently", "fastapi", "fastparquet", "flake8", "flask", "folium", "geopandas", "geopy", + "gensim", "google-cloud-aiplatform", "google-cloud-bigquery", "google-cloud-pubsub", "google-cloud-speech", "google-cloud-storage", + "google-cloud-texttospeech", "google-cloud-translate", "google-cloud-vision", "google-genai", "great-expectations", "holoviews", + "html5lib", "httpx", "huggingface_hub", "hvplot", "imbalanced-learn", "imageio", "isort", "jax", "jaxlib", + "jsonschema", # added for data validation + "langchain", "langchain_aws", "langchain_aws_bedrock", "langchain_aws_dynamodb", "langchain_aws_lambda", "langchain_aws_s3", + "langchain_aws_sagemaker", "langchain_azure", "langchain_azure_openai", "langchain_chroma", "langchain_community", + "langchain_core", "langchain_elasticsearch", "langchain_google_vertex", "langchain_huggingface", "langchain_mongodb", + "langchain_openai", "langchain_ollama", "langchain_pinecone", "langchain_redis", "langchain_sqlalchemy", + "langchain_text_splitters", "langchain_weaviate", "lightgbm", "llama-cpp-python", "lxml", "matplotlib", "mlflow", "modal", "mypy", + "mysql-connector-python", "networkx", "neuralprophet", "nltk", "numba", "numpy", "openai", "opencv-python", "optuna", "panel", + "pandas", "pendulum", "poetry", "polars", "prefect", "prophet", "psycopg2", "pillow", "pyarrow", "pydeck", + "pyjwt", "pylint", "pymongo", "pymupdf", "pyproj", "pypdf", "pypdf2", "pytest", "python-dateutil", "pytorch-lightning", + "ray", "ragas", "rapidsai-cuda11x", # optional: GPU dataframe ops + "redis", "reportlab", "requests", "rich", "ruff", "schedule", "scikit-image", "scikit-learn", "scrapy", "scipy", + "seaborn", "sentence-transformers", "shap", "shapely", "sqlite-web", "sqlalchemy", "starlette", "statsmodels", "streamlit", + "sympy", "tensorflow", "torch", "transformers", "tqdm", "typer", "vllm", "wandb", "watchdog", "xgboost", +}}) + ) + if name in ALLOWED_MODULES: + return _original_builtins['__import__'](name, *args, **kwargs) + raise ImportError(f"Module {{name!r}} is not allowed in this environment") + +_safe_builtins['__import__'] = safe_import + +try: +{self._indent_code(code)} +except Exception as e: + print(f"Error: {{e}}", file=sys.stderr) + traceback.print_exc() +""" + return safety_shim + except Exception as e: + logger.error(f"Failed to add safety shim: {str(e)}") + raise CodeExecutionError(f"Failed to prepare safe code execution: {str(e)}") + + def _indent_code(self, code: str, indent: int = 4) -> str: + return "\n".join((" " * indent) + line if line.strip() else "" for line in code.splitlines()) + + + @with_performance_tracking("async_code_execution") + @rate_limited("modal") + async def run_code_async(self, code_or_obj) -> str: + """ + Execute Python code or a code object in a Modal sandbox asynchronously. + This method supports both string code and compiled code objects, ensuring + that the code is executed in a secure, isolated environment with safety checks. + Args: + code_or_obj (str or types.CodeType): The Python code to execute, either as a string + or a compiled code object + Returns: + str: The output of the executed code, including any print statements + """ + await self._ensure_pool_initialized() + + if isinstance(code_or_obj, str): + payload = code_or_obj + elif isinstance(code_or_obj, types.CodeType): + b64 = base64.b64encode(marshal.dumps(code_or_obj)).decode() + payload = textwrap.dedent(f""" + import base64, marshal, types, traceback + code = marshal.loads(base64.b64decode({b64!r})) + try: + exec(code, {{'__name__': '__main__'}}) + except Exception: + traceback.print_exc() + """).lstrip() + else: + raise CodeExecutionError("Input must be str or types.CodeType") + + # Analyze code for required packages + start_analysis = time.time() + required_packages = self._analyze_code_dependencies(payload) + analysis_time = time.time() - start_analysis + if analysis_time > 0.1: # Only log if analysis takes significant time + logger.info(f"Code dependency analysis took {analysis_time:.2f}s") + + # Add safety shim + safe_code = self._add_safety_shim(payload) + filename = "temp_user_code.py" + write_cmd = f"cat > {filename} <<'EOF'\n{safe_code}\nEOF" + + try: + async with self.sandbox_pool.get_sandbox() as sb: + try: + # Install additional packages if needed + if required_packages: + install_start = time.time() + await self._install_packages_in_sandbox(sb, required_packages) + install_time = time.time() - install_start + logger.info(f"Package installation took {install_time:.2f}s") + + logger.info(f"Writing code to sandbox file: {filename}") + sb.exec("bash", "-c", write_cmd) + logger.info(f"Executing code from file: {filename}") + exec_start = time.time() + proc = sb.exec("python", filename) + exec_time = time.time() - exec_start + logger.info(f"Code execution took {exec_time:.2f}s") + + output = "" + if hasattr(proc, "stdout") and hasattr(proc.stdout, "read"): + output = proc.stdout.read() + if hasattr(proc, "stderr") and hasattr(proc.stderr, "read"): + output += proc.stderr.read() + else: + output = str(proc) + logger.info("Async code execution completed successfully (warm pool)") + return output + except Exception as e: + if "finished" in str(e) or "NOT_FOUND" in str(e): + logger.warning(f"Sandbox died during use, terminating: {e}") + try: + result = sb.terminate() + if asyncio.iscoroutine(result): + await result + except Exception as term_e: + logger.warning(f"Failed to terminate sandbox after error: {term_e}") + async with self.sandbox_pool.get_sandbox() as new_sb: + # Re-install packages if needed for retry + if required_packages: + await self._install_packages_in_sandbox(new_sb, required_packages) + new_sb.exec("bash", "-c", write_cmd) + proc = new_sb.exec("python", filename) + output = "" + if hasattr(proc, "stdout") and hasattr(proc.stdout, "read"): + output = proc.stdout.read() + if hasattr(proc, "stderr") and hasattr(proc.stderr, "read"): + output += proc.stderr.read() + else: + output = str(proc) + logger.info("Async code execution completed successfully on retry") + return output + else: + logger.error(f"Async code execution failed: {e}") + raise CodeExecutionError(f"Error executing code in Modal sandbox: {str(e)}") + except CodeExecutionError: + raise + except asyncio.TimeoutError: + logger.error("Async code execution timed out") + raise CodeExecutionError("Code execution timed out after 30 seconds") + except Exception as e: + logger.error(f"Async code execution failed: {str(e)}") + raise CodeExecutionError(f"Error executing code in Modal sandbox: {str(e)}") + + def _analyze_code_dependencies(self, code: str) -> List[str]: + """Analyze code to determine what packages need to be installed.""" + try: + from mcp_hub.package_utils import extract_imports_from_code, get_packages_to_install + + # Extract imports from the code + detected_imports = extract_imports_from_code(code) + logger.debug(f"Detected imports: {detected_imports}") + + # Determine what packages need to be installed + packages_to_install = get_packages_to_install(detected_imports) + + if packages_to_install: + logger.info(f"Additional packages needed: {packages_to_install}") + else: + logger.debug("No additional packages needed") + + return packages_to_install + + except Exception as e: + logger.warning(f"Failed to analyze code dependencies: {e}") + return [] + + async def _install_packages_in_sandbox(self, sandbox: modal.Sandbox, packages: List[str]): + """Install additional packages in the sandbox.""" + try: + from mcp_hub.package_utils import create_package_install_command + + install_cmd = create_package_install_command(packages) + if not install_cmd: + return + + logger.info(f"Installing packages: {' '.join(packages)}") + + # Execute pip install command + proc = await asyncio.get_event_loop().run_in_executor( + None, + lambda: sandbox.exec("bash", "-c", install_cmd, timeout=60) + ) + + # Check installation success + if hasattr(proc, 'stdout') and hasattr(proc.stdout, 'read'): + output = proc.stdout.read() + if "Successfully installed" in output or "Requirement already satisfied" in output: + logger.info("Package installation completed successfully") + else: + logger.warning(f"Package installation output: {output}") + + except Exception as e: + logger.error(f"Failed to install packages {packages}: {e}") + # Don't raise exception - continue with execution, packages might already be available + + + @with_performance_tracking("sync_code_execution") + @rate_limited("modal") + def run_code(self, code_or_obj) -> str: + """ + Execute Python code or a code object in a Modal sandbox synchronously. + This method supports both string code and compiled code objects, ensuring + that the code is executed in a secure, isolated environment with safety checks. + Args: + code_or_obj (str or types.CodeType): The Python code to execute, either as a string + or a compiled code object + Returns: + str: The output of the executed code, including any print statements + """ + try: + logger.info("Executing code synchronously in Modal sandbox") + + if isinstance(code_or_obj, str): + payload = code_or_obj + elif isinstance(code_or_obj, types.CodeType): + b64 = base64.b64encode(marshal.dumps(code_or_obj)).decode() + payload = textwrap.dedent(f""" + import base64, marshal, types, traceback + code = marshal.loads(base64.b64decode({b64!r})) + try: + exec(code, {{'__name__': '__main__'}}) + except Exception: + traceback.print_exc() + """).lstrip() + else: + raise CodeExecutionError("Input must be str or types.CodeType") + + # Add safety shim + safe_code = self._add_safety_shim(payload) + filename = "temp_user_code.py" + write_cmd = f"cat > {filename} <<'EOF'\n{safe_code}\nEOF" + + # Create sandbox synchronously + sb = None + try: + sb = modal.Sandbox.create( + app=self.app, + image=self.image, + cpu=2.0, + memory=1024, + timeout=35, + ) + + sb.exec("bash", "-c", write_cmd) + proc = sb.exec("python", filename) + output = "" + + if hasattr(proc, "stdout") and hasattr(proc.stdout, "read"): + output = proc.stdout.read() + if hasattr(proc, "stderr") and hasattr(proc.stderr, "read"): + output += proc.stderr.read() + else: + output = str(proc) + + logger.info("Sync code execution completed successfully") + return output + + + except Exception as e: + logger.warning(f"Error reading sandbox output: {e}") + output = str(proc) + + logger.info("Sync code execution completed successfully") + return output + + except CodeExecutionError: + raise + except Exception as e: + logger.error(f"Sync code execution failed: {str(e)}") + raise CodeExecutionError(f"Error executing code in Modal sandbox: {str(e)}") + + async def cleanup_pool(self): + """Cleanup the sandbox pool when shutting down.""" + if self.sandbox_pool and self._pool_initialized: + await self.sandbox_pool.stop() + logger.info("Sandbox pool cleaned up") + +class OrchestratorAgent: + """ + Main orchestrator that coordinates all agents for the complete workflow. + + This agent manages the end-to-end workflow by coordinating question enhancement, + web search, LLM processing, citation formatting, code generation, and code execution. + It provides the primary interface for complex multi-step AI-assisted tasks. + """ + + def __init__(self): + self.question_enhancer = QuestionEnhancerAgent() + self.web_search = WebSearchAgent() + self.llm_processor = LLMProcessorAgent() + self.citation_formatter = CitationFormatterAgent() + self.code_generator = CodeGeneratorAgent() + self.code_runner = CodeRunnerAgent() + + def orchestrate(self, user_request: str) -> tuple[Dict[str, Any], str]: + """ + Orchestrate the complete workflow: enhance question β†’ search β†’ generate code β†’ execute. + + Manages the full AI-assisted workflow by coordinating all agents to provide + comprehensive research, code generation, and execution. Returns both structured + data and natural language summaries of the complete process. + + Args: + user_request (str): The user's original request or question + + Returns: + tuple[Dict[str, Any], str]: A tuple containing the complete result dictionary + and a natural language summary of the process + """ + try: + logger.info(f"Starting orchestration for: {user_request[:100]}...") + + # Step 1: Enhance the question + logger.info("Step 1: Enhancing question...") + enhanced_result = self.question_enhancer.enhance_question(user_request, num_questions=3) + sub_questions = enhanced_result.get('sub_questions', [user_request]) + # Step 2: Search for information + logger.info("Step 2: Searching for information...") + search_results = [] + search_summaries = [] + + for i, question in enumerate(sub_questions[:2]): # Limit to 2 questions to avoid too many searches + logger.info(f"Processing question {i+1}: {question}") + try: + search_result = self.web_search.search(question) + logger.info(f"Search result for question {i+1}: {search_result}") + + # Extract results and summary regardless of status key + results = search_result.get('results', []) + summary = search_result.get('tavily_answer', search_result.get('summary', '')) + + if results or summary: # Treat as success if any results or summary found + logger.info(f"Question {i+1} - Found {len(results)} results") + logger.info(f"Question {i+1} - Summary: {summary[:100]}...") + + # Add to collections + search_results.extend(results) + search_summaries.append(summary) + + logger.info(f"Question {i+1} - Successfully added {len(results)} results to collection") + logger.info(f"Question {i+1} - Current total search_results: {len(search_results)}") + logger.info(f"Question {i+1} - Current total search_summaries: {len(search_summaries)}") + else: + error_msg = search_result.get('error', 'Unknown error or no results returned') + logger.warning(f"Search failed for question {i+1}: {error_msg}") + + except Exception as e: + logger.error(f"Exception during search for question '{question}': {e}") + import traceback + logger.error(f"Traceback: {traceback.format_exc()}") + + logger.info(f"Total search results collected: {len(search_results)}") + logger.info(f"Total search summaries: {len(search_summaries)}") + for i, result in enumerate(search_results[:3]): + logger.info(f"Search result {i+1}: {result.get('title', 'No title')[:50]}...") + + # Step 3: Create grounded context + logger.info("Step 3: Creating grounded context...") + grounded_context = "" + if search_results: + # Combine search results into context + context_parts = [] + for result in search_results[:5]: # Limit to top 5 results + context_parts.append(f"Title: {result.get('title', 'N/A')}") + context_parts.append(f"Content: {result.get('content', 'N/A')}") + context_parts.append(f"URL: {result.get('url', 'N/A')}") + context_parts.append("---") + + grounded_context = "\n".join(context_parts) + + # If no search results, use a generic context + if not grounded_context: + grounded_context = f"User request: {user_request}\nNo additional web search context available." + # Step 4: Generate code + logger.info("Step 4: Generating code...") + logger.info(f"Grounded context length: {len(grounded_context)}") + code_result, code_summary = self.code_generator.generate_code(user_request, grounded_context) + logger.info(f"Code generation result: {code_result}") + logger.info(f"Code generation summary: {code_summary[:200]}...") + + code_string = "" + if code_result.get('status') == 'success': + # Use raw_output (string) for display, generated_code (compiled) for execution + code_string = code_summary # This is the raw string output + logger.info(f"Successfully extracted code_string with length: {len(code_string)}") + logger.info(f"Code preview: {code_string[:200]}...") + else: + logger.warning(f"Code generation failed: {code_result.get('error', 'Unknown error')}") + + # Step 5: Execute code if available + execution_output = "" + if code_string: + logger.info("Step 5: Executing code...") + try: + # Use async execution for better performance + import asyncio + execution_output = asyncio.run(self.code_runner.run_code_async(code_string)) + except Exception as e: + execution_output = f"Execution failed: {str(e)}" + logger.warning(f"Code execution failed: {e}") + + # Step 6: Format citations + logger.info("Step 6: Formatting citations...") + citations = [] + for result in search_results: + if result.get('url'): + citations.append(f"{result.get('title', 'Untitled')} - {result.get('url')}") + # Compile final result + logger.info("=== PRE-FINAL RESULT DEBUG ===") + logger.info(f"search_results length: {len(search_results)}") + logger.info(f"search_summaries length: {len(search_summaries)}") + logger.info(f"code_string length: {len(code_string)}") + logger.info(f"execution_output length: {len(execution_output)}") + logger.info(f"citations length: {len(citations)}") + + + logger.info("=== GENERATING EXECUTIVE SUMMARY ===") + # Sample first search result + if search_results: + logger.info(f"First search result: {search_results[0]}") + + prompt = f""" + The user asked about {user_request} which yielded this summary: {search_summaries} + + During the orchestration, you generated the following code: {code_string} + + The code was executed in a secure sandbox environment, and the output was {execution_output}. + + Please provide a short and concise summary of the code that you wrote, including the user request, the summaries provided and the code generated. + Explain how the code addresses the user's request, what it does, and any important details about its execution. + + Touch upon the other methods available that were found in the search results, and how they relate to the user's request. + + Please return the result in natural language only, without any code blocks, although references to code can be made to explain why particular + code has been used, e.g. discuss why the LinerRegression module was used etc. + + If no code was generated, apologise, please state that clearly the code generation failed in the sandbox, this could be due to restriction + or the code being too complex for the sandbox to handle. + + Note, if appropriate, indicate how the code can be modified to include human input etc. as this is a banned keyword in the sandbox. + + The response should be directed at the user, in a friendly and helpful manner, as if you were a human assistant helping the user with their request. + """ + + messages = [{"role": "user", + "content": prompt}] + + logger.info(f"LLM provider is: {api_config.llm_provider}, model used: {model_config.get_model_for_provider('llm_processor', api_config.llm_provider)}") + # Last call to LLM to summarize the entire orchestration + overall_summary = make_llm_completion( + model=model_config.get_model_for_provider("llm_processor", api_config.llm_provider), + messages=messages, + temperature=app_config.llm_temperature + ) + logger.info("Overall summary generated:") + + final_result = { + "status": "success", + "user_request": user_request, + "sub_questions": sub_questions, + "search_results": search_results[:5], + "search_summaries": search_summaries, + "code_string": code_string, + "execution_output": execution_output, + "citations": citations, + "final_summary": f"{overall_summary}", + "message": "Orchestration completed successfully" + } + + # Create clean summary for display + final_narrative = f"## 🎯 Request: {user_request}\n\n{overall_summary}" + + logger.info("Orchestration completed successfully") + return final_result, final_narrative + + except (ValidationError, APIError, CodeGenerationError) as e: + logger.error(f"Orchestration failed: {str(e)}") + # Create execution log for error case + execution_log = f"Error during orchestration: {str(e)}" + return {"error": str(e), "execution_log": execution_log}, str(e) + except Exception as e: + logger.error(f"Unexpected error in orchestration: {str(e)}") + # Create execution log for error case + execution_log = f"Unexpected error: {str(e)}" + return {"error": f"Unexpected error: {str(e)}", "execution_log": execution_log}, str(e) + + def _format_search_results(self, results): + """Format search results into a combined text snippet.""" + formatted_parts = [] + for result in results: + title = result.get('title', 'No title') + content = result.get('content', 'No content') + url = result.get('url', 'No URL') + formatted_parts.append(f"Title: {title}\nContent: {content}\nURL: {url}\n---") + + return "\n".join(formatted_parts) + + async def _run_subquestion_async(self, sub_question: str, user_request: str) -> tuple: + """Process a single sub-question asynchronously.""" + try: + # Search + search_result = await self.web_search.search_async(sub_question) + if search_result.get("error"): + logger.warning(f"Async search failed for sub-question: {search_result['error']}") + return None, None + + # Format search results + results = search_result.get("results", [])[:app_config.max_search_results] + formatted_text = self._format_search_results(results) + + # Process search results + llm_summary = await self.llm_processor.async_process( + formatted_text, + "summarize", + f"Context of user request: {user_request}" + ) + + # Prepare result + result_data = { + "status": "success", + "sub_question": sub_question, + "user_request": user_request, + "search_results": results, + "search_summary": llm_summary.get('llm_processed_output', '') + } + + # Create summary parts + summary_parts = [] + summary_parts.append(f"## Subquestion: {sub_question}") + summary_parts.append("### Research Summary:") + summary_parts.append(llm_summary.get('llm_processed_output', 'No summary available')) + + # Add sources if available + citations = [] + for result in results: + if result.get('url'): + citations.append(f"{result.get('title', 'Untitled')} - {result.get('url')}") + + if citations: + summary_parts.append("### Sources:") + for i, citation in enumerate(citations, 1): + summary_parts.append(f"{i}. {citation}") + + clean_summary = "\n\n".join(summary_parts) + + logger.info("Subquestion processing completed successfully") + return result_data, clean_summary + + except Exception as e: + logger.error(f"Subquestion processing failed: {e}") + error_result = { + "status": "error", + "user_request": user_request, + "sub_question": sub_question, + "error": str(e), + "message": "Subquestion processing failed" + } + return error_result, f"❌ Error: {str(e)}" + +# Initialize individual agents +question_enhancer = QuestionEnhancerAgent() +web_search = WebSearchAgent() +llm_processor = LLMProcessorAgent() +citation_formatter = CitationFormatterAgent() +code_generator = CodeGeneratorAgent() +code_runner = CodeRunnerAgent() + +# Initialize orchestrator +orchestrator = OrchestratorAgent() + +# ---------------------------------------- +# Advanced Feature Functions +# ---------------------------------------- + +# Wrapper functions for backward compatibility with existing Gradio interface +def agent_orchestrator(user_request: str) -> tuple: + """ + Wrapper for OrchestratorAgent with async-first approach and sync fallback. + + Provides a unified interface to the orchestrator that attempts async execution + for better performance and falls back to synchronous execution if needed. + Handles event loop management and thread pooling automatically. + + Args: + user_request (str): The user's request to be processed + + Returns: + tuple: A tuple containing the orchestration result and summary + """ + try: + # Try async orchestration first for better performance + if hasattr(orchestrator, "orchestrate_async"): + try: + # Check if we're in an async context + loop = asyncio.get_event_loop() + if loop.is_running(): + # If loop is already running (like in Gradio), we need to handle this differently + # Use asyncio.run_coroutine_threadsafe or run in thread pool + import concurrent.futures + + def run_async_in_thread(): + # Create a new event loop for this thread + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + return new_loop.run_until_complete(orchestrator.orchestrate_async(user_request)) + finally: + new_loop.close() + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(run_async_in_thread) + result = future.result() + else: + # No loop running, safe to use run_until_complete + result = loop.run_until_complete(orchestrator.orchestrate_async(user_request)) + + logger.info("Successfully used async orchestration") + return result + + except RuntimeError as e: + if "cannot be called from a running event loop" in str(e): + logger.warning("Cannot use asyncio.run from running event loop, trying thread approach") + # Fallback: run in a separate thread + import concurrent.futures + + def run_async_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + return new_loop.run_until_complete(orchestrator.orchestrate_async(user_request)) + finally: + new_loop.close() + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(run_async_in_thread) + return future.result() + else: + raise + + except Exception as e: + logger.warning(f"Async orchestration failed: {e}. Falling back to sync.") + + # Fallback to synchronous orchestration + logger.info("Using synchronous orchestration as fallback") + return orchestrator.orchestrate(user_request) + +def agent_orchestrator_dual_output(user_request: str) -> tuple: + """Wrapper for OrchestratorAgent that returns both JSON and natural language output. + Provides a unified interface to the orchestrator that returns structured data + and a natural language summary of the orchestration process. + Args: + user_request (str): The user's request to be processed + + Returns: + tuple: A tuple containing the orchestration result as a JSON dictionary + and a natural language summary of the process + """ + result = orchestrator.orchestrate(user_request) + + # Extract the natural language summary from the result + if isinstance(result, tuple) and len(result) > 0: + json_result = result[0] if result[0] else {} + + # Create a natural language summary + if isinstance(json_result, dict): + summary = json_result.get('final_summary', '') + if not summary: + summary = json_result.get('summary', '') + if not summary and 'code_output' in json_result: + summary = f"Code executed successfully. Output: {json_result.get('code_output', {}).get('output', 'No output')}" + if not summary: + summary = "Process completed successfully." + else: + summary = "Process completed successfully." + else: + summary = "No results available." + json_result = {} + + # Start warmup in background thread using the start_sandbox_warmup function + start_sandbox_warmup() + + return json_result, summary + +# ---------------------------------------- +# Advanced Feature Functions +# ---------------------------------------- + +def get_health_status() -> Dict[str, Any]: + """ + Get comprehensive system health status including advanced monitoring features. + + Retrieves detailed health information about the system including availability + of advanced features, system resources, and operational metrics. Returns + basic information if advanced monitoring is not available. + + Returns: + Dict[str, Any]: A dictionary containing system health status and metrics + """ + if not ADVANCED_FEATURES_AVAILABLE: + return { + "status": "basic_mode", + "message": "Advanced features not available. Install 'pip install psutil aiohttp' to enable health monitoring.", + "system_info": { + "python_version": f"{types.__module__}", + "gradio_available": True, + "modal_available": True + } + } + + try: + return health_monitor.get_health_stats() + except Exception as e: + return {"error": f"Health monitoring failed: {str(e)}"} + +def get_performance_metrics() -> Dict[str, Any]: + """ + Get performance metrics and analytics for the MCP Hub system. + + Collects and returns performance metrics including execution times, + success rates, error counts, and resource utilization. Provides + basic information if advanced metrics collection is not available. + + Returns: + Dict[str, Any]: A dictionary containing performance metrics and statistics + """ + if not ADVANCED_FEATURES_AVAILABLE: + return { + "status": "basic_mode", + "message": "Performance metrics not available. Install 'pip install psutil aiohttp' to enable advanced monitoring.", + "basic_info": { + "system_working": True, + "features_loaded": False + } + } + try: + return metrics_collector.get_metrics_summary() + except Exception as e: + return {"error": f"Performance metrics failed: {str(e)}"} + +def get_cache_status() -> Dict[str, Any]: + """Get cache status and statistics.""" + if not ADVANCED_FEATURES_AVAILABLE: + return { + "status": "basic_mode", + "message": "Cache monitoring not available. Install 'pip install psutil aiohttp' to enable cache statistics.", + "cache_info": { + "caching_available": False, + "recommendation": "Install advanced features for intelligent caching" + } + } + + try: + from mcp_hub.cache_utils import cache_manager + return cache_manager.get_cache_status() + except Exception as e: + return {"error": f"Cache status failed: {str(e)}"} + +async def get_sandbox_pool_status() -> Dict[str, Any]: + """Get sandbox pool status and statistics.""" + try: + # Create a temporary code runner to get pool stats + code_runner = CodeRunnerAgent() + stats = await code_runner.get_pool_stats() + + # Add warmup status information + pool_size = stats.get("pool_size", 0) + target_size = stats.get("target_pool_size", 0) + + if pool_size == 0: + status_message = "πŸ”„ Sandbox environment is warming up... This may take up to 2 minutes for the first execution." + status = "warming_up" + elif pool_size < target_size: + status_message = f"⚑ Sandbox pool partially ready ({pool_size}/{target_size} sandboxes). More sandboxes warming up..." + status = "partially_ready" + else: + status_message = f"βœ… Sandbox pool fully ready ({pool_size}/{target_size} sandboxes available)" + status = "ready" + + return { + "status": status, + "sandbox_pool": stats, + "message": status_message, + "user_message": status_message + } + except Exception as e: + return { + "status": "error", + "error": f"Failed to get sandbox pool status: {str(e)}", + "message": "Sandbox pool may not be initialized yet", + "user_message": "πŸ”„ Code execution environment is starting up... Please wait a moment." + } + +def get_sandbox_pool_status_sync() -> Dict[str, Any]: + """Synchronous wrapper for sandbox pool status.""" + try: + import asyncio + return asyncio.run(get_sandbox_pool_status()) + except Exception as e: + return {"error": f"Failed to get sandbox pool status: {str(e)}"} + +def start_sandbox_warmup(): + """Start background sandbox warmup task.""" + try: + import asyncio + import threading + + def warmup_task(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + # Create a code runner to initialize the pool + code_runner = CodeRunnerAgent() + loop.run_until_complete(code_runner._ensure_pool_initialized()) + logger.info("Sandbox pool warmed up successfully") + except Exception as e: + logger.warning(f"Failed to warm up sandbox pool: {e}") + finally: + loop.close() + + # Start warmup in background thread + warmup_thread = threading.Thread(target=warmup_task, daemon=True) + warmup_thread.start() + logger.info("Started background sandbox warmup") + + except Exception as e: + logger.warning(f"Failed to start sandbox warmup: {e}") + +class IntelligentCacheManager: + """ + Advanced caching system for MCP Hub operations with TTL and eviction policies. + + Provides intelligent caching capabilities with time-to-live (TTL) support, + automatic eviction of expired entries, and comprehensive cache statistics. + Optimizes performance by caching operation results and managing memory usage. + """ + + def __init__(self): + self.cache = {} + self.cache_stats = { + 'hits': 0, + 'misses': 0, + 'total_requests': 0 + } + self.max_cache_size = 1000 + self.default_ttl = 3600 # 1 hour + def _generate_cache_key(self, operation: str, **kwargs) -> str: + """ + Generate a unique cache key based on operation and parameters. + + Creates a deterministic cache key by combining the operation name with + parameter values. Uses MD5 hashing to ensure consistent key generation + while keeping keys manageable in size. + + Args: + operation (str): The operation name to include in the cache key + **kwargs: Parameter values to include in the key generation + + Returns: + str: A unique cache key as an MD5 hash string + """ + import hashlib + key_data = f"{operation}:{json.dumps(kwargs, sort_keys=True)}" + return hashlib.md5(key_data.encode()).hexdigest() + + def get(self, operation: str, **kwargs): + """ + Retrieve cached data for a specific operation with automatic cleanup. + + Fetches cached data for the given operation and parameters. Automatically + removes expired entries and updates cache statistics. Returns None if no + valid cached data is found. + + Args: + operation (str): The operation name to look up in cache + **kwargs: Parameter values used to generate the cache key + + Returns: + Any: The cached data if found and valid, otherwise None + """ + cache_key = self._generate_cache_key(operation, **kwargs) + self.cache_stats['total_requests'] += 1 + + if cache_key in self.cache: + entry = self.cache[cache_key] + current_time = time.time() + + if current_time < entry['expires_at']: + self.cache_stats['hits'] += 1 + logger.info(f"Cache hit for operation: {operation}") + return entry['data'] + else: + # Remove expired entry + del self.cache[cache_key] + + self.cache_stats['misses'] += 1 + return None + + def set(self, operation: str, data: Any, ttl: int = None, **kwargs): + """Cache the result with TTL.""" + cache_key = self._generate_cache_key(operation, **kwargs) + expires_at = time.time() + (ttl or self.default_ttl) + + # Remove oldest entries if cache is full + if len(self.cache) >= self.max_cache_size: + self._evict_oldest_entries(int(self.max_cache_size * 0.1)) + + self.cache[cache_key] = { + 'data': data, + 'expires_at': expires_at, + 'created_at': time.time() + } + logger.info(f"Cached result for operation: {operation}") + + def _evict_oldest_entries(self, count: int): + """Remove the oldest entries from cache.""" + sorted_items = sorted( + self.cache.items(), + key=lambda x: x[1]['created_at'] + ) + for i in range(min(count, len(sorted_items))): + del self.cache[sorted_items[i][0]] + + def get_stats(self) -> Dict[str, Any]: + """Get cache performance statistics.""" + hit_rate = (self.cache_stats['hits'] / max(1, self.cache_stats['total_requests'])) * 100 + return { + 'cache_size': len(self.cache), + 'max_cache_size': self.max_cache_size, + 'hit_rate': round(hit_rate, 2), + 'total_hits': self.cache_stats['hits'], + 'total_misses': self.cache_stats['misses'], + 'total_requests': self.cache_stats['total_requests'] + } + + def clear(self): + """Clear all cached entries.""" + self.cache.clear() + logger.info("Cache cleared") + + +def agent_research_request(user_request): + """ + This function researches a coding request from the user, generates code, executes it, + and returns a clean summary of the results. + + This is an mcp server function that responds to research coding requests from users. + + Args: + user_request (str): The user's request or question to be processed + Returns: + tuple: A tuple containing the JSON result from the orchestrator and a clean summary + """ + # Get the full response (which is a tuple) + orchestrator_result = agent_orchestrator(user_request) + + # Extract the JSON result (first element of tuple) + if isinstance(orchestrator_result, tuple) and len(orchestrator_result) > 0: + json_result = orchestrator_result[0] + else: + json_result = orchestrator_result + + # Extract and format the clean output + clean_summary = "" + if isinstance(json_result, dict): + if 'final_summary' in json_result: + clean_summary += f"## πŸ“‹ Summary\n{json_result['final_summary']}\n\n" + if 'code_string' in json_result and json_result['code_string']: + clean_summary += f"## πŸ’» Generated Code\n```python\n{json_result['code_string']}\n```\n\n" + + if 'execution_output' in json_result and json_result['execution_output']: + clean_summary += f"## ▢️ Execution Result\n```\n{json_result['execution_output']}\n```\n\n" + + if 'code_output' in json_result and json_result['code_output']: + # Handle both string and dict formats for code_output + code_output = json_result['code_output'] + if isinstance(code_output, dict): + output = code_output.get('output', '') + else: + output = str(code_output) + + if output: + clean_summary += f"## ▢️ Code Output\n```\n{output}\n```\n\n" + + if 'citations' in json_result and json_result['citations']: + clean_summary += "## πŸ“š Sources\n" + for i, citation in enumerate(json_result['citations'], 1): + clean_summary += f"{i}. {citation}\n" + clean_summary += "\n" + + if 'sub_questions' in json_result: + clean_summary += "## πŸ” Research Questions Explored\n" + for i, q in enumerate(json_result['sub_questions'], 1): + clean_summary += f"{i}. {q}\n" + + # If we have sub-summaries, show them too + if 'sub_summaries' in json_result and json_result['sub_summaries']: + clean_summary += "\n## πŸ“– Research Summaries\n" + for i, summary in enumerate(json_result['sub_summaries'], 1): + clean_summary += f"### {i}. {summary}...\n" + + if not clean_summary: + clean_summary = "## ⚠️ Processing Complete\nThe request was processed but no detailed results were generated." + + return json_result, clean_summary +# ---------------------------------------- +# Gradio UI / MCP Server Setup +# ---------------------------------------- + +def agent_question_enhancer(user_request: str) -> dict: + """ + Wrapper for QuestionEnhancerAgent to provide question enhancement. + + Args: + user_request (str): The original user request to enhance + + Returns: + dict: Enhanced question result with sub-questions + """ + return question_enhancer.enhance_question(user_request, num_questions=2) + +def agent_web_search(query: str) -> dict: + """ + Wrapper for WebSearchAgent to perform web searches. + + Args: + query (str): The search query to execute + + Returns: + dict: Web search results with summaries and URLs + """ + return web_search.search(query) + +def agent_llm_processor(text_input: str, task: str, context: str | None = None) -> dict: + """ + Wrapper for LLMProcessorAgent to process text with LLM. + + Args: + text_input (str): The input text to process + task (str): The processing task ('summarize', 'reason', or 'extract_keywords') + context (str | None): Optional context for processing + + Returns: + dict: LLM processing result with output and metadata + """ + return llm_processor.process(text_input, task, context) + +def agent_citation_formatter(text_block: str) -> dict: + """ + Wrapper for CitationFormatterAgent to format citations. + + Args: + text_block (str): The text containing URLs to cite + + Returns: + dict: Formatted citations result with APA-style references + """ + return citation_formatter.format_citations(text_block) + +def agent_code_generator(user_request: str, grounded_context: str) -> tuple: + """ + Wrapper for CodeGeneratorAgent to generate Python code. + + Args: + user_request (str): The user's request for code generation + grounded_context (str): Context information to guide generation + + Returns: + tuple: A tuple containing the generation result and raw code + """ + return code_generator.generate_code(user_request, grounded_context) + +def code_runner_wrapper(code_or_obj) -> str: + """ + Wrapper for CodeRunnerAgent that uses async execution with warm pool. + + Provides a simplified interface to the code runner with automatic sandbox + pool management and user-friendly error messages. Handles warm-up status + checks and provides appropriate feedback during startup. + + Args: + code_or_obj: The code string or object to be executed + + Returns: + str: The execution result or user-friendly error message + """ + try: + import asyncio + + # First check sandbox pool status to provide user feedback + try: + pool_status = asyncio.run(get_sandbox_pool_status()) + user_message = pool_status.get("user_message", "") + if pool_status.get("status") == "warming_up": + return f"{user_message}\n\nPlease try again in a moment once the environment is ready." + except Exception: + pass # Continue with execution even if status check fails + + # Use async execution to leverage the warm sandbox pool + result = asyncio.run(code_runner.run_code_async(code_or_obj)) + return result + except CodeExecutionError as e: + error_msg = str(e) + if "Failed to get sandbox" in error_msg or "timeout" in error_msg.lower(): + return "πŸ”„ The code execution environment is still starting up. Please wait a moment and try again.\n\nThis is normal for the first execution after startup (can take 1-2 minutes)." + return error_msg + except Exception as e: + logger.error(f"Code runner wrapper error: {e}") + return f"Error: {str(e)}" + + +def research_code(user_request: str) -> tuple: + """ + This function serves as an MCP (Model Context Protocol) tool that orchestrates + comprehensive research and code generation workflows. It enhances user requests + through intelligent processing, performs web searches for relevant information, + generates appropriate code solutions, executes the code safely, and provides + clean, actionable summaries. + The function is designed to be used as a tool within MCP frameworks, providing + autonomous research capabilities that combine web search, code generation, and + execution in a single workflow. + user_request (str): The user's request, question, or problem statement to be + processed. Can include coding problems, research questions, + or requests for information gathering and analysis. + tuple: A two-element tuple containing: + - JSON result (dict): Structured data from the orchestrator containing + detailed research findings, generated code, execution results, and + metadata about the research process + - Clean summary (str): A human-readable summary of the research findings + and generated solutions, formatted for easy consumption + Example: + >>> result, summary = research_code("How to implement a binary search in Python?") + >>> print(summary) # Clean explanation with code examples + >>> print(result['code']) # Generated code implementation + Note: + This function is optimized for use as an MCP tool and handles error cases + gracefully, returning meaningful feedback even when research or code + generation encounters issues. + """ + return agent_research_request(user_request) + +CUSTOM_CSS = """ +.app-title { + text-align: center; + font-family: 'Roboto', sans-serif; + font-size: 3rem; + font-weight: 700; + letter-spacing: 1px; + color: #10b981; + text-shadow: 1px 1px 2px rgba(0,0,0,0.4); + border-bottom: 4px solid #4f46e5; + display: inline-block; + padding-bottom: 0.5rem; + margin: 2rem auto 1.5rem; + max-width: 90%; +} +""" + + +with gr.Blocks(title="Shallow Research Code Assistant Hub", + theme=gr.themes.Ocean(), + fill_width=False, + css=CUSTOM_CSS) as demo: + + with gr.Row(): + with gr.Column(): + gr.Markdown( + """ +

+ Shallow Research Code Assistant Hub +

+ """, + container=False, + ) + + with gr.Row(): + with gr.Column(scale=1, min_width=320): + gr.Markdown( + """ +

Welcome

+ This hub provides a streamlined interface for AI-assisted research and code generation. + It integrates multiple agents to enhance your coding and research workflow. + + The application can be accessed via the MCP server at: + +

+ """, + container=True, + height=200, + ) + + with gr.Column(scale=1, min_width=320): + gr.Image( + value="static/CodeAssist.png", + label="MCP Hub Logo", + height=200, + show_label=False, + elem_id="mcp_hub_logo" + ) + + gr.Markdown( + """ +

Agents And Flows:

+ """ + ) + + with gr.Tab("Orchestrator Flow", scale=1): + gr.Markdown("## AI Research & Code Assistant") + gr.Markdown(""" + **Workflow:** Splits into two or more sub-questions β†’ Tavily search & summarization β†’ Generate Python code β†’ Execute via Modal β†’ Return results with citations + """) + + with gr.Row(): + with gr.Column(scale=1, min_width=320): + input_textbox = gr.Textbox( + label="Your High-Level Request", lines=12, + placeholder="Describe the code you need or the research topic you want to explore…", + ) + process_btn = gr.Button("πŸš€ Process Request", variant="primary", size="lg") + + json_output = gr.JSON(label="Complete Orchestrated Output", + container=True, + height=300, + ) + with gr.Column(scale=1, min_width=300): + with gr.Accordion("πŸ”Ž Show detailed summary", open=True): + clean_output = gr.Markdown(label="Summary & Results") + + process_btn.click( + fn=agent_research_request, + inputs=[input_textbox], + outputs=[json_output, clean_output], + ) + + with gr.Tab("Agent: Question Enhancer", scale=1): + gr.Interface( + fn=agent_question_enhancer, + inputs=[ + gr.Textbox( + label="Original User Request", + lines=12, + placeholder="Enter your question to be split into 3 sub-questions…" + ) + ], + outputs=gr.JSON(label="Enhanced Sub-Questions", + height=305), + title="Question Enhancer Agent", + description="Splits a single user query into 3 distinct sub-questions using Qwen models.", + api_name="agent_question_enhancer_service", + ) + + with gr.Tab("Agent: Web Search", scale=1): + gr.Interface( + fn=agent_web_search, + inputs=[gr.Textbox(label="Search Query", placeholder="Enter search term…", lines=12)], + outputs=gr.JSON(label="Web Search Results (Tavily)", height=305), + title="Web Search Agent", + description="Perform a Tavily web search with configurable result limits.", + api_name="agent_web_search_service", + ) + + with gr.Tab("Agent: LLM Processor", scale=1): + gr.Interface( + fn=agent_llm_processor, + inputs=[ + gr.Textbox(label="Text to Process", lines=12, placeholder="Enter text for the LLM…"), + gr.Dropdown( + choices=["summarize", "reason", "extract_keywords"], + value="summarize", + label="LLM Task", + ), + gr.Textbox(label="Optional Context", lines=12, placeholder="Background info…"), + ], + outputs=gr.JSON(label="LLM Processed Output", height=1200), + title="LLM Processing Agent", + description="Use configured LLM provider for text processing tasks.", + api_name="agent_llm_processor_service", + ) + + with gr.Tab("Agent: Citation Formatter", scale=1): + gr.Interface( + fn=agent_citation_formatter, + inputs=[gr.Textbox(label="Text Block with Citations", lines=12, placeholder="Enter text to format citations…")], + outputs=gr.JSON(label="Formatted Citations", height=305), + title="Citation Formatter Agent", + description="Extracts and formats APA-style citations from text blocks.", + api_name="agent_citation_formatter_service", + ) + with gr.Tab("Agent: Code Generator", scale=1): + gr.Interface( + fn=agent_code_generator, + inputs=[ + gr.Textbox(label="User Request", lines=12, placeholder="Describe the code you need…"), + gr.Textbox(label="Grounded Context", lines=12, placeholder="Context for code generation…") + ], + outputs=gr.JSON(label="Generated Code", height=610), + title="Code Generation Agent", + description="Generates Python code based on user requests and context.", + api_name="agent_code_generator_service", + ) + with gr.Tab("Agent: Code Runner", scale=1): + gr.Interface( + fn=code_runner_wrapper, + inputs=[gr.Textbox(label="Code to Execute", lines=12, placeholder="Enter Python code to run…")], + outputs=gr.Textbox(label="Execution Output", lines=12), + title="Code Runner Agent", + description="Executes Python code in a secure environment and returns the output.", + api_name="agent_code_runner_service", + ) + + with gr.Tab("Advanced Features", scale=1): + gr.Markdown("## Advanced Features") + gr.Markdown(""" + **Available Features**: + - **Health Monitoring**: System health and performance metrics. + - **Performance Analytics**: Detailed performance statistics. + - **Intelligent Caching**: Advanced caching system for improved efficiency. + - **Sandbox Pool Status**: Monitor warm sandbox pool performance and statistics. + + **Note**: Some features require additional dependencies. Install with `pip install psutil aiohttp` to enable all features. + """) + + with gr.Row(): + health_btn = gr.Button("Get Health Status", variant="primary") + metrics_btn = gr.Button("Get Performance Metrics", variant="primary") + cache_btn = gr.Button("Get Cache Status", variant="primary") + sandbox_btn = gr.Button("Get Sandbox Pool Status", variant="primary") + + health_output = gr.JSON(label="Health Status") + metrics_output = gr.JSON(label="Performance Metrics") + cache_output = gr.JSON(label="Cache Status") + sandbox_output = gr.JSON(label="Sandbox Pool Status") + + health_btn.click( + fn=get_health_status, + inputs=[], + outputs=health_output, + api_name="get_health_status_service" + ) + + metrics_btn.click( + fn=get_performance_metrics, + inputs=[], + outputs=metrics_output, + api_name="get_performance_metrics_service" + ) + + cache_btn.click( + fn=get_cache_status, + inputs=[], + outputs=cache_output, + api_name="get_cache_status_service" + ) + + sandbox_btn.click( + fn=get_sandbox_pool_status_sync, + inputs=[], + outputs=sandbox_output, + api_name="get_sandbox_pool_status_service" + ) + +# ---------------------------------------- +# Main Entry Point +# ---------------------------------------- +if __name__ == "__main__": + import signal + import atexit + + # Start the background warmup task for sandbox pool + start_sandbox_warmup() + + # Register cleanup functions for graceful shutdown + def cleanup_on_exit(): + """Cleanup function to run on exit.""" + try: + import asyncio + + # Attempt to cleanup sandbox pool + def run_cleanup(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + code_runner = CodeRunnerAgent() + if code_runner._pool_initialized: + loop.run_until_complete(code_runner.cleanup_pool()) + logger.info("Sandbox pool cleaned up on exit") + except Exception as e: + logger.warning(f"Failed to cleanup sandbox pool on exit: {e}") + finally: + loop.close() + + run_cleanup() + except Exception as e: + logger.warning(f"Error during cleanup: {e}") + + # Register cleanup handlers + atexit.register(cleanup_on_exit) + + def signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info(f"Received signal {signum}, initiating cleanup...") + cleanup_on_exit() + exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + demo.launch( + mcp_server=True, + server_name="127.0.0.1", + server_port=7860, + show_error=True, + share=False + ) + except KeyboardInterrupt: + logger.info("Application interrupted by user") + cleanup_on_exit() + except Exception as e: + logger.error(f"Application error: {e}") + cleanup_on_exit() + raise + diff --git a/mcp_api_call.py b/mcp_api_call.py new file mode 100644 index 0000000000000000000000000000000000000000..2c3c231632ffdc5e572565c97eb4bbccd9688b8d --- /dev/null +++ b/mcp_api_call.py @@ -0,0 +1,39 @@ +from gradio_client import Client + +def print_human_readable_result(result): + # Print main request and status + if isinstance(result, tuple): + result = next((item for item in result if isinstance(item, dict)), result[0]) + print("Status:", result.get('status', 'N/A')) + print("Status:", result.get('status', 'N/A')) + print("User Request:", result.get('user_request', 'N/A')) + print("\nSub-Questions:") + for i, sub_q in enumerate(result.get('sub_questions', []), 1): + print(f" {i}. {sub_q}") + + print("\nSearch Summaries:") + for i, summary in enumerate(result.get('search_summaries', []), 1): + print(f" {i}. {summary}") + + print("\nSearch Results:") + for i, res in enumerate(result.get('search_results', []), 1): + print(f" {i}. {res['title']}\n URL: {res['url']}\n Content: {res['content'][:100]}{'...' if len(res['content']) > 100 else ''}\n Score: {res['score']:.3f}") + + print("\nGenerated Code:\n" + result.get('code_string', 'N/A')) + + print("\nExecution Output:\n" + result.get('execution_output', 'N/A')) + + print("\nCitations:") + for i, cit in enumerate(result.get('citations', []), 1): + print(f" {i}. {cit}") + + print("\nFinal Summary:\n" + result.get('final_summary', 'N/A')) + + print("\nOrchestration Message:", result.get('message', 'N/A')) + +client = Client("http://127.0.0.1:7860/") +result = client.predict( + user_request="How do I calculate the sum of an array in Python?", + api_name="/process_orchestrator_request" +) +print_human_readable_result(result) \ No newline at end of file diff --git a/mcp_hub/__init__.py b/mcp_hub/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b48d8e9e605bcea12ff141d65da2c4d562b0d564 --- /dev/null +++ b/mcp_hub/__init__.py @@ -0,0 +1,20 @@ +"""MCP Hub - Multi-Agent Communication Protocol Hub for Research and Code Generation.""" + +__version__ = "1.0.0" +__author__ = "Your Name" +__description__ = "Advanced MCP Hub with intelligent agent orchestration" + +# Core imports that should be available at package level +try: + from .config import api_config, model_config, app_config + from .exceptions import APIError, ValidationError, CodeGenerationError, CodeExecutionError + from .logging_config import logger + + __all__ = [ + "api_config", "model_config", "app_config", + "APIError", "ValidationError", "CodeGenerationError", "CodeExecutionError", + "logger" + ] +except ImportError: + # Graceful degradation for missing dependencies + __all__ = [] diff --git a/mcp_hub/__pycache__/__init__.cpython-312.pyc b/mcp_hub/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f519eda7befc4f41d3fdc841b6040509ac4f0325 Binary files /dev/null and b/mcp_hub/__pycache__/__init__.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/cache_utils.cpython-312.pyc b/mcp_hub/__pycache__/cache_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a61950de94f26cfa31b7a97e27e4ca0ff9472bf Binary files /dev/null and b/mcp_hub/__pycache__/cache_utils.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/config.cpython-312.pyc b/mcp_hub/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbbf048f0a2606a686f983018bcbe9e0aa5b93f8 Binary files /dev/null and b/mcp_hub/__pycache__/config.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/exceptions.cpython-312.pyc b/mcp_hub/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b8f5c00747c34f74fe88bf304a4faaed259f139 Binary files /dev/null and b/mcp_hub/__pycache__/exceptions.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/health_monitoring.cpython-312.pyc b/mcp_hub/__pycache__/health_monitoring.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddc803ad180e1961ea8dffdf5539475821988661 Binary files /dev/null and b/mcp_hub/__pycache__/health_monitoring.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/logging_config.cpython-312.pyc b/mcp_hub/__pycache__/logging_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28588596970bdbba9a7147ccb4c57fad958fd3f7 Binary files /dev/null and b/mcp_hub/__pycache__/logging_config.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/package_utils.cpython-312.pyc b/mcp_hub/__pycache__/package_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e9aead1f1aaf001a5f11b3a4b4903183e53cddc Binary files /dev/null and b/mcp_hub/__pycache__/package_utils.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/performance_monitoring.cpython-312.pyc b/mcp_hub/__pycache__/performance_monitoring.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b231d5fd3b20d0e64f183f5eac92257c8f0b1ad Binary files /dev/null and b/mcp_hub/__pycache__/performance_monitoring.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/reliability_utils.cpython-312.pyc b/mcp_hub/__pycache__/reliability_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8dc4cf7914984a5f3ef340a30b9830f199df3c02 Binary files /dev/null and b/mcp_hub/__pycache__/reliability_utils.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/sandbox_pool.cpython-312.pyc b/mcp_hub/__pycache__/sandbox_pool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..058eb7a2dfb3a900fb2832ca2fb747017d6e0d1c Binary files /dev/null and b/mcp_hub/__pycache__/sandbox_pool.cpython-312.pyc differ diff --git a/mcp_hub/__pycache__/utils.cpython-312.pyc b/mcp_hub/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7b41dfe91582d75dda142ed578e4f89d0a5c902 Binary files /dev/null and b/mcp_hub/__pycache__/utils.cpython-312.pyc differ diff --git a/mcp_hub/advanced_config.py b/mcp_hub/advanced_config.py new file mode 100644 index 0000000000000000000000000000000000000000..ce4870e9f63c5307915968f229ab2cb78230d537 --- /dev/null +++ b/mcp_hub/advanced_config.py @@ -0,0 +1,272 @@ +"""Advanced configuration management with validation and environment-specific settings.""" + +import os +import json +from pathlib import Path +from typing import Dict, Any, Optional +from dataclasses import dataclass, field +from .logging_config import logger + +@dataclass +class APIConfig: + """API configuration with validation.""" + nebius_api_key: str = "" + nebius_base_url: str = "https://api.studio.nebius.ai/v1/" + tavily_api_key: str = "" + + # API-specific settings + nebius_model: str = "meta-llama/Meta-Llama-3.1-8B-Instruct" + nebius_max_tokens: int = 1000 + nebius_temperature: float = 0.7 + + tavily_search_depth: str = "basic" + tavily_max_results: int = 5 + + def __post_init__(self): + """Validate configuration after initialization.""" + if not self.nebius_api_key: + raise ValueError("NEBIUS_API_KEY is required") + if not self.tavily_api_key: + raise ValueError("TAVILY_API_KEY is required") + + # Validate numeric ranges + if not 0.0 <= self.nebius_temperature <= 2.0: + raise ValueError("nebius_temperature must be between 0.0 and 2.0") + if self.nebius_max_tokens <= 0: + raise ValueError("nebius_max_tokens must be positive") + if self.tavily_max_results <= 0: + raise ValueError("tavily_max_results must be positive") + +@dataclass +class AppConfig: + """Application configuration.""" + environment: str = "development" # development, staging, production + debug: bool = True + log_level: str = "INFO" + + # Gradio settings + gradio_server_name: str = "0.0.0.0" + gradio_server_port: int = 7860 + gradio_share: bool = False + gradio_auth: Optional[tuple] = None + + # Performance settings + max_search_results: int = 10 + max_sub_questions: int = 5 + cache_ttl_seconds: int = 3600 + request_timeout_seconds: int = 30 + + # Rate limiting + api_calls_per_second: float = 2.0 + api_burst_size: int = 5 + + # Circuit breaker settings + circuit_breaker_failure_threshold: int = 5 + circuit_breaker_timeout_seconds: int = 60 + + # Monitoring settings + metrics_retention_hours: int = 24 + health_check_interval_seconds: int = 300 # 5 minutes + + def __post_init__(self): + """Validate application configuration.""" + valid_environments = ["development", "staging", "production"] + if self.environment not in valid_environments: + raise ValueError(f"environment must be one of: {valid_environments}") + + valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + if self.log_level not in valid_log_levels: + raise ValueError(f"log_level must be one of: {valid_log_levels}") + + if self.gradio_server_port <= 0 or self.gradio_server_port > 65535: + raise ValueError("gradio_server_port must be between 1 and 65535") + +@dataclass +class SecurityConfig: + """Security configuration.""" + enable_authentication: bool = False + allowed_origins: list = field(default_factory=lambda: ["*"]) + api_key_header: str = "X-API-Key" + rate_limit_per_ip: int = 100 # requests per hour + max_request_size_mb: int = 10 + + # Content filtering + enable_content_filtering: bool = True + blocked_patterns: list = field(default_factory=list) + + def __post_init__(self): + """Validate security configuration.""" + if self.rate_limit_per_ip <= 0: + raise ValueError("rate_limit_per_ip must be positive") + if self.max_request_size_mb <= 0: + raise ValueError("max_request_size_mb must be positive") + +class ConfigManager: + """Centralized configuration management with environment-specific overrides.""" + + def __init__(self, config_dir: str = "config"): + """ + Initialize configuration manager. + + Args: + config_dir: Directory containing configuration files + """ + self.config_dir = Path(config_dir) + self.config_dir.mkdir(exist_ok=True) + + # Load environment variables + self._load_environment_variables() + + # Initialize configurations + self.api_config = self._load_api_config() + self.app_config = self._load_app_config() + self.security_config = self._load_security_config() + + logger.info(f"Configuration loaded for environment: {self.app_config.environment}") + + def _load_environment_variables(self): + """Load environment variables from .env file if it exists.""" + env_file = Path(".env") + if env_file.exists(): + from dotenv import load_dotenv + load_dotenv() + logger.info("Loaded environment variables from .env file") + + def _load_api_config(self) -> APIConfig: + """Load API configuration from environment and config files.""" + # Start with environment variables + config_data = { + "nebius_api_key": os.getenv("NEBIUS_API_KEY", ""), + "nebius_base_url": os.getenv("NEBIUS_BASE_URL", "https://api.studio.nebius.ai/v1/"), + "tavily_api_key": os.getenv("TAVILY_API_KEY", ""), + "nebius_model": os.getenv("NEBIUS_MODEL", "meta-llama/Meta-Llama-3.1-8B-Instruct"), + "nebius_max_tokens": int(os.getenv("NEBIUS_MAX_TOKENS", "1000")), + "nebius_temperature": float(os.getenv("NEBIUS_TEMPERATURE", "0.7")), + "tavily_search_depth": os.getenv("TAVILY_SEARCH_DEPTH", "basic"), + "tavily_max_results": int(os.getenv("TAVILY_MAX_RESULTS", "5")) + } + + # Override with config file if it exists + config_file = self.config_dir / "api_config.json" + if config_file.exists(): + try: + with open(config_file, 'r') as f: + file_config = json.load(f) + config_data.update(file_config) + logger.info("Loaded API configuration from config file") + except Exception as e: + logger.warning(f"Failed to load API config file: {e}") + + return APIConfig(**config_data) + + def _load_app_config(self) -> AppConfig: + """Load application configuration.""" + environment = os.getenv("ENVIRONMENT", "development") + + # Base configuration + config_data = { + "environment": environment, + "debug": environment == "development", + "log_level": os.getenv("LOG_LEVEL", "INFO"), + "gradio_server_name": os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), + "gradio_server_port": int(os.getenv("GRADIO_SERVER_PORT", "7860")), + "gradio_share": os.getenv("GRADIO_SHARE", "false").lower() == "true", + "max_search_results": int(os.getenv("MAX_SEARCH_RESULTS", "10")), + "max_sub_questions": int(os.getenv("MAX_SUB_QUESTIONS", "5")), + "cache_ttl_seconds": int(os.getenv("CACHE_TTL_SECONDS", "3600")), + "request_timeout_seconds": int(os.getenv("REQUEST_TIMEOUT_SECONDS", "30")) + } + + # Environment-specific overrides + env_config_file = self.config_dir / f"app_config_{environment}.json" + if env_config_file.exists(): + try: + with open(env_config_file, 'r') as f: + env_config = json.load(f) + config_data.update(env_config) + logger.info(f"Loaded environment-specific config: {environment}") + except Exception as e: + logger.warning(f"Failed to load environment config: {e}") + + return AppConfig(**config_data) + + def _load_security_config(self) -> SecurityConfig: + """Load security configuration.""" + config_data = { + "enable_authentication": os.getenv("ENABLE_AUTH", "false").lower() == "true", + "rate_limit_per_ip": int(os.getenv("RATE_LIMIT_PER_IP", "100")), + "max_request_size_mb": int(os.getenv("MAX_REQUEST_SIZE_MB", "10")), + "enable_content_filtering": os.getenv("ENABLE_CONTENT_FILTERING", "true").lower() == "true" + } + + # Load from config file + config_file = self.config_dir / "security_config.json" + if config_file.exists(): + try: + with open(config_file, 'r') as f: + file_config = json.load(f) + config_data.update(file_config) + logger.info("Loaded security configuration from config file") + except Exception as e: + logger.warning(f"Failed to load security config: {e}") + + return SecurityConfig(**config_data) + + def save_config_template(self): + """Save configuration templates for easy editing.""" + templates = { + "api_config.json": { + "nebius_model": "meta-llama/Meta-Llama-3.1-8B-Instruct", + "nebius_max_tokens": 1000, + "nebius_temperature": 0.7, + "tavily_search_depth": "basic", + "tavily_max_results": 5 + }, + "app_config_development.json": { + "debug": True, + "log_level": "DEBUG", + "gradio_share": False, + "max_search_results": 5 + }, + "app_config_production.json": { + "debug": False, + "log_level": "INFO", + "gradio_share": False, + "max_search_results": 10, + "cache_ttl_seconds": 7200 + }, + "security_config.json": { + "enable_authentication": False, + "allowed_origins": ["*"], + "rate_limit_per_ip": 100, + "enable_content_filtering": True, + "blocked_patterns": [] + } + } + + for filename, template in templates.items(): + config_file = self.config_dir / filename + if not config_file.exists(): + try: + with open(config_file, 'w') as f: + json.dump(template, f, indent=2) + logger.info(f"Created config template: {filename}") + except Exception as e: + logger.error(f"Failed to create config template {filename}: {e}") + + def get_config_summary(self) -> Dict[str, Any]: + """Get a summary of current configuration (without sensitive data).""" + return { + "environment": self.app_config.environment, + "debug_mode": self.app_config.debug, + "log_level": self.app_config.log_level, + "gradio_port": self.app_config.gradio_server_port, + "cache_ttl": self.app_config.cache_ttl_seconds, + "max_search_results": self.app_config.max_search_results, + "authentication_enabled": self.security_config.enable_authentication, + "content_filtering_enabled": self.security_config.enable_content_filtering, + "api_endpoints": { + "nebius": bool(self.api_config.nebius_api_key), + "tavily": bool(self.api_config.tavily_api_key) + } + } diff --git a/mcp_hub/async_utils.py b/mcp_hub/async_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..976ac74e17c178ebe488b7d659e371846e971887 --- /dev/null +++ b/mcp_hub/async_utils.py @@ -0,0 +1,95 @@ +"""Async utilities for improved performance in concurrent operations.""" + +import asyncio +import aiohttp +from typing import Dict, Any, List +from concurrent.futures import ThreadPoolExecutor +from .config import api_config, app_config +from .exceptions import APIError +from .logging_config import logger + +class AsyncWebSearchAgent: + """Async version of web search for concurrent operations.""" + + def __init__(self): + self.session = None + + async def __aenter__(self): + """Async context manager entry.""" + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self.session: + await self.session.close() + + async def search_multiple_queries(self, queries: List[str]) -> List[Dict[str, Any]]: + """Search multiple queries concurrently.""" + if not self.session: + raise APIError("AsyncWebSearch", "Session not initialized. Use as async context manager.") + + logger.info(f"Starting concurrent search for {len(queries)} queries") + + # Create tasks for concurrent execution + tasks = [self._search_single_query(query) for query in queries] + + # Execute all searches concurrently + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Process results and handle any exceptions + processed_results = [] + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.error(f"Search failed for query {i}: {str(result)}") + processed_results.append({ + "error": str(result), + "query": queries[i], + "results": [] + }) + else: + processed_results.append(result) + + logger.info(f"Completed concurrent searches: {len([r for r in processed_results if not r.get('error')])} successful") + return processed_results + + async def _search_single_query(self, query: str) -> Dict[str, Any]: + """Search a single query using Tavily API.""" + try: + # In a real implementation, you'd make async HTTP calls to Tavily + # For now, we'll use the sync version in a thread pool + from tavily import TavilyClient + client = TavilyClient(api_key=api_config.tavily_api_key) + + # Run sync operation in thread pool + loop = asyncio.get_event_loop() + with ThreadPoolExecutor() as executor: + response = await loop.run_in_executor( + executor, + lambda: client.search( + query=query, + search_depth="basic", + max_results=app_config.max_search_results, + include_answer=True + ) + ) + + return { + "query": response.get("query", query), + "tavily_answer": response.get("answer"), + "results": response.get("results", []), + "data_source": "Tavily Search API (Async)", + } + + except Exception as e: + raise APIError("Tavily", f"Async search failed: {str(e)}") + +async def process_subquestions_concurrently(sub_questions: List[str]) -> List[Dict[str, Any]]: + """Process multiple sub-questions concurrently for better performance.""" + logger.info(f"Processing {len(sub_questions)} sub-questions concurrently") + + async with AsyncWebSearchAgent() as async_searcher: + # Execute all searches concurrently + search_results = await async_searcher.search_multiple_queries(sub_questions) + + return search_results diff --git a/mcp_hub/cache_utils.py b/mcp_hub/cache_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..72c44f759ade477c0cc0045c0fb82b0c2e7cefe3 --- /dev/null +++ b/mcp_hub/cache_utils.py @@ -0,0 +1,211 @@ +"""Caching system for improved performance and reduced API calls.""" + +import hashlib +import json +import pickle +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, Dict, Optional, Callable +from functools import wraps +from .logging_config import logger + +class CacheManager: + """Simple file-based cache manager for API responses and computations.""" + + def __init__(self, cache_dir: str = "cache", default_ttl: int = 3600): + """ + Initialize cache manager. + + Args: + cache_dir: Directory to store cache files + default_ttl: Default time-to-live in seconds (1 hour default) + """ + self.cache_dir = Path(cache_dir) + self.cache_dir.mkdir(exist_ok=True) + self.default_ttl = default_ttl + logger.info(f"Cache manager initialized with directory: {self.cache_dir}") + + def _get_cache_key(self, func_name: str, args: tuple, kwargs: dict) -> str: + """Generate a unique cache key based on function name and arguments.""" + # Create a string representation of arguments + key_data = { + "func": func_name, + "args": args, + "kwargs": kwargs + } + key_string = json.dumps(key_data, sort_keys=True, default=str) + return hashlib.md5(key_string.encode()).hexdigest() + + def _get_cache_path(self, cache_key: str) -> Path: + """Get the file path for a cache key.""" + return self.cache_dir / f"{cache_key}.cache" + + def get(self, cache_key: str) -> Optional[Any]: + """Retrieve a value from cache if it exists and is not expired.""" + cache_path = self._get_cache_path(cache_key) + + if not cache_path.exists(): + return None + + try: + with open(cache_path, 'rb') as f: + cache_data = pickle.load(f) + + # Check if cache has expired + if datetime.now() > cache_data['expires_at']: + logger.debug(f"Cache expired for key: {cache_key}") + cache_path.unlink() # Delete expired cache + return None + + logger.debug(f"Cache hit for key: {cache_key}") + return cache_data['value'] + + except (EOFError, pickle.PickleError, KeyError) as e: + logger.warning(f"Cache corruption for key {cache_key}: {e}") + cache_path.unlink() # Delete corrupted cache + return None + + def set(self, cache_key: str, value: Any, ttl: Optional[int] = None) -> None: + """Store a value in cache with optional TTL.""" + if ttl is None: + ttl = self.default_ttl + + cache_data = { + 'value': value, + 'created_at': datetime.now(), + 'expires_at': datetime.now() + timedelta(seconds=ttl) + } + + cache_path = self._get_cache_path(cache_key) + + try: + with open(cache_path, 'wb') as f: + pickle.dump(cache_data, f) + logger.debug(f"Cached value for key: {cache_key} (TTL: {ttl}s)") + except Exception as e: + logger.error(f"Failed to cache value for key {cache_key}: {e}") + + def cached_call(self, func: Callable, args: tuple, kwargs: dict, ttl: Optional[int] = None) -> Any: + """Make a cached function call.""" + cache_key = self._get_cache_key(func.__name__, args, kwargs) + + # Try to get from cache first + cached_result = self.get(cache_key) + if cached_result is not None: + return cached_result + + # Execute function and cache result + logger.debug(f"Cache miss for {func.__name__}, executing function") + result = func(*args, **kwargs) + self.set(cache_key, result, ttl) + + return result + + def clear_expired(self) -> int: + """Remove all expired cache files and return count of removed files.""" + removed_count = 0 + current_time = datetime.now() + + for cache_file in self.cache_dir.glob("*.cache"): + try: + with open(cache_file, 'rb') as f: + cache_data = pickle.load(f) + + if current_time > cache_data['expires_at']: + cache_file.unlink() + removed_count += 1 + + except Exception as e: + logger.warning(f"Error checking cache file {cache_file}: {e}") + cache_file.unlink() # Remove corrupted files + removed_count += 1 + + if removed_count > 0: + logger.info(f"Removed {removed_count} expired cache files") + + return removed_count + + def clear_all(self) -> int: + """Remove all cache files and return count of removed files.""" + removed_count = 0 + for cache_file in self.cache_dir.glob("*.cache"): + cache_file.unlink() + removed_count += 1 + + logger.info(f"Cleared all cache: removed {removed_count} files") + return removed_count + + def get_cache_status(self) -> Dict[str, Any]: + """Get detailed status information about the cache system.""" + try: + # Count cache files + cache_files = list(self.cache_dir.glob("*.cache")) + cache_count = len(cache_files) + + # Calculate cache directory size + total_size = sum(f.stat().st_size for f in cache_files) + + # Count expired files + expired_count = 0 + current_time = datetime.now() + for cache_file in cache_files: + try: + with open(cache_file, 'rb') as f: + cache_data = pickle.load(f) + + if current_time > cache_data['expires_at']: + expired_count += 1 + except Exception: + expired_count += 1 # Count corrupted files as expired + + # Get cache stats + return { + "status": "healthy", + "cache_dir": str(self.cache_dir), + "total_files": cache_count, + "expired_files": expired_count, + "total_size_bytes": total_size, + "total_size_mb": round(total_size / (1024 * 1024), 2), + "default_ttl_seconds": self.default_ttl, + "timestamp": datetime.now().isoformat() + } + except Exception as e: + logger.error(f"Failed to get cache status: {str(e)}") + return { + "status": "error", + "error": str(e), + "timestamp": datetime.now().isoformat() + } + +# Global cache manager instance +cache_manager = CacheManager() + +def cached(ttl: int = 3600): + """ + Decorator to cache function results. + + Args: + ttl: Time-to-live in seconds + """ + def decorator(func: Callable): + @wraps(func) + def wrapper(*args, **kwargs): + return cache_manager.cached_call(func, args, kwargs, ttl) + return wrapper + return decorator + +# Specialized caching functions for common operations +@cached(ttl=1800) # 30 minutes +def cached_web_search(query: str) -> Dict[str, Any]: + """Cached version of web search - import happens at runtime.""" + # Import at runtime to avoid circular imports + from tavily import TavilyClient + client = TavilyClient(api_key="placeholder") # Will be replaced at runtime + # This is a placeholder - actual implementation would use the real agent + return {"query": query, "results": [], "cached": True} + +@cached(ttl=3600) # 1 hour +def cached_llm_processing(text_input: str, task: str, context: Optional[str] = None) -> Dict[str, Any]: + """Cached version of LLM processing - import happens at runtime.""" + # This is a placeholder for the caching pattern + return {"input_text": text_input, "task": task, "cached": True} diff --git a/mcp_hub/config.py b/mcp_hub/config.py new file mode 100644 index 0000000000000000000000000000000000000000..7d068e5e0e6338b2eedabe89168e372ec9f0c6c7 --- /dev/null +++ b/mcp_hub/config.py @@ -0,0 +1,120 @@ +"""Configuration management for the MCP Hub project.""" + +import os +from dataclasses import dataclass +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +@dataclass +class APIConfig: + """API configuration settings.""" + # Provider selection + llm_provider: str = "nebius" # Options: "nebius", "openai", "anthropic", "huggingface" + + # Provider API keys + nebius_api_key: str = "" + openai_api_key: str = "" + anthropic_api_key: str = "" + huggingface_api_key: str = "" + + # Other APIs + tavily_api_key: str = "" + + # Provider URLs + nebius_base_url: str = "https://api.studio.nebius.com/v1/" + huggingface_base_url: str = "https://api-inference.huggingface.co" + + # Other settings + current_year: str = "2025" + + def __post_init__(self): + """Validate required API keys based on selected provider.""" + # Always require Tavily for search functionality + if not self.tavily_api_key or not self.tavily_api_key.startswith("tvly-"): + raise RuntimeError("A valid TAVILY_API_KEY is required in your .env file.") + + # Validate LLM provider selection + valid_providers = ["nebius", "openai", "anthropic", "huggingface"] + if self.llm_provider not in valid_providers: + raise RuntimeError(f"LLM_PROVIDER must be one of: {', '.join(valid_providers)}") + + # Validate required API key for selected provider + if self.llm_provider == "nebius" and not self.nebius_api_key: + raise RuntimeError("NEBIUS_API_KEY is required when using nebius provider.") + elif self.llm_provider == "openai" and not self.openai_api_key: + raise RuntimeError("OPENAI_API_KEY is required when using openai provider.") + elif self.llm_provider == "anthropic" and not self.anthropic_api_key: + raise RuntimeError("ANTHROPIC_API_KEY is required when using anthropic provider.") + elif self.llm_provider == "huggingface" and not self.huggingface_api_key: + raise RuntimeError("HUGGINGFACE_API_KEY is required when using huggingface provider.") + +@dataclass +class ModelConfig: + """Model configuration settings.""" + # Default models (Nebius/HuggingFace compatible) + question_enhancer_model: str = "Qwen/Qwen3-4B-fast" + llm_processor_model: str = "meta-llama/Meta-Llama-3.1-8B-Instruct" + code_generator_model: str = "Qwen/Qwen2.5-Coder-32B-Instruct-fast" + orchestrator_model: str = "Qwen/Qwen3-32B-fast" + + def get_model_for_provider(self, task: str, provider: str) -> str: + """Get appropriate model for the given task and provider.""" + + # Model mappings by provider + provider_models = { + "nebius": { + "question_enhancer": self.question_enhancer_model, + "llm_processor": self.llm_processor_model, + "code_generator": self.code_generator_model, + "orchestrator": self.orchestrator_model, + }, + "openai": { + "question_enhancer": "gpt-4.1-nano", + "llm_processor": "gpt-4.1-nano", + "code_generator": "gpt-4.1", + "orchestrator": "gpt-4.1", + }, + "anthropic": { + "question_enhancer": "claude-3-5-haiku-latest",# + "llm_processor": "claude-3-5-sonnet-latest", + "code_generator": "claude-sonnet-4-0", + "orchestrator": "claude-sonnet-4-0", + }, + "huggingface": { + "question_enhancer": "microsoft/phi-4", + "llm_processor": "microsoft/phi-4", + "code_generator": "Qwen/Qwen2.5-Coder-32B-Instruct", + "orchestrator": "microsoft/phi-4", + } + } + + if provider not in provider_models: + # Fall back to default models + return getattr(self, f"{task}_model", self.llm_processor_model) + + return provider_models[provider].get(task, provider_models[provider]["llm_processor"]) + +@dataclass +class AppConfig: + """Application configuration settings.""" + modal_app_name: str = "my-sandbox-app" + max_search_results: int = 2 + max_code_generation_attempts: int = 3 + llm_temperature: float = 0.6 + code_gen_temperature: float = 0.1 + +# Create global configuration instances +api_config = APIConfig( + llm_provider=os.environ.get("LLM_PROVIDER", "nebius"), + nebius_api_key=os.environ.get("NEBIUS_API_KEY", ""), + openai_api_key=os.environ.get("OPENAI_API_KEY", ""), + anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY", ""), + huggingface_api_key=os.environ.get("HUGGINGFACE_API_KEY", ""), + tavily_api_key=os.environ.get("TAVILY_API_KEY", ""), + current_year=os.environ.get("CURRENT_YEAR", "2025") +) + +model_config = ModelConfig() +app_config = AppConfig() diff --git a/mcp_hub/exceptions.py b/mcp_hub/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..7eb45ca5d5816f0b219f5bbc2e2ba15ebd991ad8 --- /dev/null +++ b/mcp_hub/exceptions.py @@ -0,0 +1,28 @@ +"""Custom exception classes for the MCP Hub project.""" + +class MCPHubError(Exception): + """Base exception class for MCP Hub errors.""" + pass + +class APIError(MCPHubError): + """Raised when API calls fail.""" + def __init__(self, service: str, message: str): + self.service = service + self.message = message + super().__init__(f"{service} API Error: {message}") + +class ConfigurationError(MCPHubError): + """Raised when there are configuration issues.""" + pass + +class ValidationError(MCPHubError): + """Raised when input validation fails.""" + pass + +class CodeGenerationError(MCPHubError): + """Raised when code generation fails.""" + pass + +class CodeExecutionError(MCPHubError): + """Raised when code execution fails.""" + pass diff --git a/mcp_hub/health_monitoring.py b/mcp_hub/health_monitoring.py new file mode 100644 index 0000000000000000000000000000000000000000..a4ee89d5b1499e5bef3ae4fb13eb62b360c5c1e7 --- /dev/null +++ b/mcp_hub/health_monitoring.py @@ -0,0 +1,261 @@ +"""System health monitoring and status dashboard functionality.""" + +import time +import psutil +from datetime import datetime +from typing import Dict, Any +from .config import api_config +from .logging_config import logger +from .reliability_utils import health_monitor +from .performance_monitoring import metrics_collector + +class SystemHealthChecker: + """Comprehensive system health checking.""" + + def __init__(self): + self.last_check = None + self.health_status = {} + + def check_api_connectivity(self) -> Dict[str, Any]: + """Check connectivity to external APIs.""" + results = {} + + # Check Nebius API + try: + from openai import OpenAI + client = OpenAI( + api_key=api_config.nebius_api_key, + base_url=api_config.nebius_base_url + ) + + start_time = time.time() + # Make a minimal test call + response = client.chat.completions.create( + model="meta-llama/Meta-Llama-3.1-8B-Instruct", + messages=[{"role": "user", "content": "test"}], + max_tokens=1 + ) + response_time = time.time() - start_time + + results["nebius"] = { + "status": "healthy", + "response_time_ms": response_time * 1000, + "last_checked": datetime.now().isoformat() + } + + except Exception as e: + results["nebius"] = { + "status": "unhealthy", + "error": str(e), + "last_checked": datetime.now().isoformat() + } + + # Check Tavily API + try: + from tavily import TavilyClient + client = TavilyClient(api_key=api_config.tavily_api_key) + + start_time = time.time() + # Make a minimal test search + response = client.search(query="test", max_results=1) + response_time = time.time() - start_time + + results["tavily"] = { + "status": "healthy", + "response_time_ms": response_time * 1000, + "last_checked": datetime.now().isoformat() + } + + except Exception as e: + results["tavily"] = { + "status": "unhealthy", + "error": str(e), + "last_checked": datetime.now().isoformat() + } + + return results + + def check_system_resources(self) -> Dict[str, Any]: + """Check system resource usage.""" + try: + # CPU usage + cpu_percent = psutil.cpu_percent(interval=1) + + # Memory usage + memory = psutil.virtual_memory() + + # Disk usage + disk = psutil.disk_usage('/') + + # Process-specific metrics + process = psutil.Process() + process_memory = process.memory_info() + + return { + "cpu_percent": cpu_percent, + "memory": { + "total_gb": memory.total / (1024**3), + "available_gb": memory.available / (1024**3), + "percent_used": memory.percent + }, + "disk": { + "total_gb": disk.total / (1024**3), + "free_gb": disk.free / (1024**3), + "percent_used": (disk.used / disk.total) * 100 + }, + "process": { + "memory_mb": process_memory.rss / (1024**2), + "cpu_percent": process.cpu_percent() + }, + "status": "healthy", + "last_checked": datetime.now().isoformat() + } + + except Exception as e: + return { + "status": "unhealthy", + "error": str(e), + "last_checked": datetime.now().isoformat() + } + + def check_cache_health(self) -> Dict[str, Any]: + """Check cache system health.""" + try: + from cache_utils import cache_manager + + # Count cache files + cache_files = list(cache_manager.cache_dir.glob("*.cache")) + + # Calculate cache directory size + total_size = sum(f.stat().st_size for f in cache_files) + + return { + "cache_files_count": len(cache_files), + "total_size_mb": total_size / (1024**2), + "cache_directory": str(cache_manager.cache_dir), + "status": "healthy", + "last_checked": datetime.now().isoformat() + } + + except Exception as e: + return { + "status": "unhealthy", + "error": str(e), + "last_checked": datetime.now().isoformat() + } + + def get_comprehensive_health_report(self) -> Dict[str, Any]: + """Get a comprehensive health report of the entire system.""" + logger.info("Generating comprehensive health report") + + report = { + "timestamp": datetime.now().isoformat(), + "overall_status": "healthy" # Will be updated based on checks + } + + # Check API connectivity + api_health = self.check_api_connectivity() + report["api_connectivity"] = api_health + + # Check system resources + system_health = self.check_system_resources() + report["system_resources"] = system_health + + # Check cache health + cache_health = self.check_cache_health() + report["cache_system"] = cache_health + + # Get API health stats from monitor + try: + nebius_stats = health_monitor.get_health_stats("nebius") + tavily_stats = health_monitor.get_health_stats("tavily") + + report["api_performance"] = { + "nebius": nebius_stats, + "tavily": tavily_stats + } + except Exception as e: + report["api_performance"] = {"error": str(e)} + + # Get performance metrics + try: + performance_summary = metrics_collector.get_metrics_summary() + report["performance_metrics"] = performance_summary + except Exception as e: + report["performance_metrics"] = {"error": str(e)} + + # Determine overall status + unhealthy_components = [] + + for service, status in api_health.items(): + if status.get("status") == "unhealthy": + unhealthy_components.append(f"API:{service}") + + if system_health.get("status") == "unhealthy": + unhealthy_components.append("system_resources") + + if cache_health.get("status") == "unhealthy": + unhealthy_components.append("cache_system") + + if unhealthy_components: + report["overall_status"] = "degraded" + report["unhealthy_components"] = unhealthy_components + + self.last_check = datetime.now() + self.health_status = report + + logger.info(f"Health report generated: {report['overall_status']}") + return report + +# Global health checker instance +health_checker = SystemHealthChecker() + +def create_health_dashboard() -> str: + """Create a formatted health dashboard for display.""" + report = health_checker.get_comprehensive_health_report() + + dashboard = f""" +# πŸ₯ System Health Dashboard +**Last Updated:** {report['timestamp']} +**Overall Status:** {'🟒' if report['overall_status'] == 'healthy' else '🟑' if report['overall_status'] == 'degraded' else 'πŸ”΄'} {report['overall_status'].upper()} + +## 🌐 API Connectivity +""" + + for service, status in report.get("api_connectivity", {}).items(): + status_icon = "🟒" if status.get("status") == "healthy" else "πŸ”΄" + response_time = status.get("response_time_ms", 0) + dashboard += f"- **{service.title()}:** {status_icon} {status.get('status', 'unknown')} ({response_time:.1f}ms)\n" + + dashboard += "\n## πŸ’» System Resources\n" + sys_resources = report.get("system_resources", {}) + if "memory" in sys_resources: + memory = sys_resources["memory"] + dashboard += f"- **Memory:** {memory['percent_used']:.1f}% used ({memory['available_gb']:.1f}GB available)\n" + + if "cpu_percent" in sys_resources: + dashboard += f"- **CPU:** {sys_resources['cpu_percent']:.1f}% usage\n" + + if "process" in sys_resources: + process = sys_resources["process"] + dashboard += f"- **Process Memory:** {process['memory_mb']:.1f}MB\n" + + dashboard += "\n## πŸ“Š Performance Metrics\n" + perf_metrics = report.get("performance_metrics", {}) + if perf_metrics and not perf_metrics.get("error"): + for metric_name, metric_data in perf_metrics.items(): + if isinstance(metric_data, dict) and "average" in metric_data: + dashboard += f"- **{metric_name}:** Avg: {metric_data['average']:.3f}, Count: {metric_data['count']}\n" + + dashboard += "\n## πŸ”§ Cache System\n" + cache_info = report.get("cache_system", {}) + if cache_info.get("status") == "healthy": + dashboard += f"- **Cache Files:** {cache_info.get('cache_files_count', 0)} files\n" + dashboard += f"- **Cache Size:** {cache_info.get('total_size_mb', 0):.1f}MB\n" + + if report.get("unhealthy_components"): + dashboard += "\n## ⚠️ Issues Detected\n" + for component in report["unhealthy_components"]: + dashboard += f"- {component}\n" + + return dashboard diff --git a/mcp_hub/logging_config.py b/mcp_hub/logging_config.py new file mode 100644 index 0000000000000000000000000000000000000000..462b9749ee626994471ed829ecfd1d7f3814aabd --- /dev/null +++ b/mcp_hub/logging_config.py @@ -0,0 +1,51 @@ +"""Logging configuration for the MCP Hub project.""" + +import logging +import sys +from datetime import datetime +from pathlib import Path + +def setup_logging( + log_level: str = "INFO", + log_to_file: bool = True, + log_dir: str = "logs" +) -> logging.Logger: + """Set up logging configuration.""" + + # Create logs directory if it doesn't exist + if log_to_file: + log_path = Path(log_dir) + log_path.mkdir(exist_ok=True) + + # Create logger + logger = logging.getLogger("mcp_hub") + logger.setLevel(getattr(logging, log_level.upper())) + + # Clear any existing handlers + logger.handlers = [] + + # Create formatter + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s" + ) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(getattr(logging, log_level.upper())) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # File handler + if log_to_file: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + file_handler = logging.FileHandler( + log_path / f"mcp_hub_{timestamp}.log" + ) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + +# Create global logger instance +logger = setup_logging() diff --git a/mcp_hub/package_utils.py b/mcp_hub/package_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..90a963620f7e0f067a03fcef908ffe3291b58e21 --- /dev/null +++ b/mcp_hub/package_utils.py @@ -0,0 +1,192 @@ +""" +Package management utilities for dynamic package installation in Modal sandboxes. +This module provides functions to analyze code for imports and manage package installation. +""" +import ast +import re +from typing import Set, List + +try: + from mcp_hub.logging_config import logger +except ImportError: + # Fallback logger for testing/standalone use + import logging + logger = logging.getLogger(__name__) + + +# Core packages that should be preinstalled in the base image +CORE_PREINSTALLED_PACKAGES = { + "numpy", "pandas", "matplotlib", "requests", "json", "os", "sys", + "time", "datetime", "math", "random", "collections", "itertools", + "functools", "re", "urllib", "csv", "sqlite3", "pathlib", "typing", + "asyncio", "threading", "multiprocessing", "subprocess", "shutil", + "tempfile", "io", "gzip", "zipfile", "tarfile", "base64", "hashlib", + "secrets", "uuid", "pickle", "copy", "operator", "bisect", "heapq", + "contextlib", "weakref", "gc", "inspect", "types", "enum", "dataclasses", + "decimal", "fractions", "statistics", "string", "textwrap", "locale", + "calendar", "timeit", "argparse", "getopt", "logging", "warnings", + "platform", "signal", "errno", "ctypes", "struct", "array", "queue", + "socketserver", "http", "urllib2", "html", "xml", "email", "mailbox" +} + +# Extended packages that can be dynamically installed +COMMON_PACKAGES = { + "scikit-learn": "sklearn", + "beautifulsoup4": "bs4", + "pillow": "PIL", + "opencv-python-headless": "cv2", + "python-dateutil": "dateutil", + "plotly": "plotly", + "seaborn": "seaborn", + "polars": "polars", + "lightgbm": "lightgbm", + "xgboost": "xgboost", + "flask": "flask", + "fastapi": "fastapi", + "httpx": "httpx", + "networkx": "networkx", + "wordcloud": "wordcloud", + "textblob": "textblob", + "spacy": "spacy", + "nltk": "nltk" +} + +# Map import names to package names +IMPORT_TO_PACKAGE = {v: k for k, v in COMMON_PACKAGES.items()} +IMPORT_TO_PACKAGE.update({k: k for k in COMMON_PACKAGES.keys()}) + + +def extract_imports_from_code(code_str: str) -> Set[str]: + """ + Extract all import statements from Python code using AST parsing. + + Args: + code_str: The Python code to analyze + + Returns: + Set of imported module names (top-level only) + """ + imports = set() + + try: + tree = ast.parse(code_str) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + # Get top-level module name + module_name = alias.name.split('.')[0] + imports.add(module_name) + elif isinstance(node, ast.ImportFrom): + if node.module: + # Get top-level module name + module_name = node.module.split('.')[0] + imports.add(module_name) + except Exception as e: + logger.warning(f"Failed to parse code with AST, falling back to regex: {e}") + # Fallback to regex-based extraction + imports.update(extract_imports_with_regex(code_str)) + + return imports + + +def extract_imports_with_regex(code_str: str) -> Set[str]: + """ + Fallback method to extract imports using regex patterns. + + Args: + code_str: The Python code to analyze + + Returns: + Set of imported module names + """ + imports = set() + + # Pattern for "import module" statements + import_pattern = r'^import\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)' + + # Pattern for "from module import ..." statements + from_pattern = r'^from\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+import' + + for line in code_str.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + + # Check for import statements + import_match = re.match(import_pattern, line) + if import_match: + module_name = import_match.group(1).split('.')[0] + imports.add(module_name) + continue + + # Check for from...import statements + from_match = re.match(from_pattern, line) + if from_match: + module_name = from_match.group(1).split('.')[0] + imports.add(module_name) + + return imports + + +def get_packages_to_install(detected_imports: Set[str]) -> List[str]: + """ + Determine which packages need to be installed based on detected imports. + + Args: + detected_imports: Set of module names found in the code + + Returns: + List of package names that need to be pip installed + """ + packages_to_install = [] + + for import_name in detected_imports: + # Skip if it's a core preinstalled package + if import_name in CORE_PREINSTALLED_PACKAGES: + continue + + # Check if we have a known package mapping + if import_name in IMPORT_TO_PACKAGE: + package_name = IMPORT_TO_PACKAGE[import_name] + packages_to_install.append(package_name) + # For unknown packages, assume package name matches import name + elif import_name not in CORE_PREINSTALLED_PACKAGES: + packages_to_install.append(import_name) + + return packages_to_install + + +def get_warmup_import_commands() -> List[str]: + """ + Get list of import commands to run during sandbox warmup. + + Returns: + List of Python import statements for core packages + """ + core_imports = [ + "import numpy", + "import pandas", + "import matplotlib.pyplot", + "import requests", + "print('Core packages warmed up successfully')" + ] + + return core_imports + + +def create_package_install_command(packages: List[str]) -> str: + """ + Create a pip install command for the given packages. + + Args: + packages: List of package names to install + + Returns: + Pip install command string + """ + if not packages: + return "" + + # Remove duplicates and sort + unique_packages = sorted(set(packages)) + return f"pip install {' '.join(unique_packages)}" \ No newline at end of file diff --git a/mcp_hub/performance_monitoring.py b/mcp_hub/performance_monitoring.py new file mode 100644 index 0000000000000000000000000000000000000000..7fdeaf82c0b26a9821e5baf920714b845c477cdc --- /dev/null +++ b/mcp_hub/performance_monitoring.py @@ -0,0 +1,232 @@ +"""Performance monitoring and metrics collection for the MCP Hub.""" + +import time +import psutil +import threading +from datetime import datetime, timedelta +from typing import Dict, Any, Optional +from collections import defaultdict, deque +from dataclasses import dataclass +from contextlib import contextmanager +from .logging_config import logger + +@dataclass +class MetricPoint: + """Single metric measurement.""" + timestamp: datetime + metric_name: str + value: float + tags: Dict[str, str] + +class MetricsCollector: + """Collects and stores application metrics.""" + + def __init__(self, max_points: int = 10000): + """ + Initialize metrics collector. + + Args: + max_points: Maximum number of metric points to store + """ + self.max_points = max_points + self.metrics = defaultdict(lambda: deque(maxlen=max_points)) + self.lock = threading.Lock() + self.counters = defaultdict(int) + self.timers = {} + + # Start system metrics collection thread + self.system_thread = threading.Thread(target=self._collect_system_metrics, daemon=True) + self.system_thread.start() + logger.info("Metrics collector initialized") + + def record_metric(self, name: str, value: float, tags: Optional[Dict[str, str]] = None): + """Record a metric value.""" + if tags is None: + tags = {} + + point = MetricPoint( + timestamp=datetime.now(), + metric_name=name, + value=value, + tags=tags + ) + + with self.lock: + self.metrics[name].append(point) + + def increment_counter(self, name: str, amount: int = 1, tags: Optional[Dict[str, str]] = None): + """Increment a counter metric.""" + with self.lock: + self.counters[name] += amount + + self.record_metric(f"{name}_count", self.counters[name], tags) + + @contextmanager + def timer(self, name: str, tags: Optional[Dict[str, str]] = None): + """Context manager for timing operations.""" + start_time = time.time() + try: + yield + finally: + duration = time.time() - start_time + self.record_metric(f"{name}_duration_seconds", duration, tags) + + def get_metrics_summary(self, + metric_name: Optional[str] = None, + last_minutes: int = 5) -> Dict[str, Any]: + """Get summary statistics for metrics.""" + cutoff_time = datetime.now() - timedelta(minutes=last_minutes) + + with self.lock: + if metric_name: + metrics_to_analyze = {metric_name: self.metrics[metric_name]} + else: + metrics_to_analyze = dict(self.metrics) + + summary = {} + + for name, points in metrics_to_analyze.items(): + recent_points = [p for p in points if p.timestamp >= cutoff_time] + + if not recent_points: + continue + + values = [p.value for p in recent_points] + summary[name] = { + "count": len(values), + "average": sum(values) / len(values), + "min": min(values), + "max": max(values), + "latest": values[-1] if values else 0, + "last_updated": recent_points[-1].timestamp.isoformat() if recent_points else None + } + + return summary + + def _collect_system_metrics(self): + """Background thread to collect system metrics.""" + while True: + try: + # CPU and memory metrics + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + + self.record_metric("system_cpu_percent", cpu_percent) + self.record_metric("system_memory_percent", memory.percent) + self.record_metric("system_memory_available_mb", memory.available / 1024 / 1024) + + # Process-specific metrics + process = psutil.Process() + process_memory = process.memory_info() + + self.record_metric("process_memory_rss_mb", process_memory.rss / 1024 / 1024) + self.record_metric("process_cpu_percent", process.cpu_percent()) + + time.sleep(30) # Collect every 30 seconds + + except Exception as e: + logger.error(f"Error collecting system metrics: {e}") + time.sleep(60) # Wait longer if there's an error + +class PerformanceProfiler: + """Profile performance of agent operations.""" + + def __init__(self, metrics_collector: MetricsCollector): + self.metrics = metrics_collector + self.operation_stats = defaultdict(list) + + @contextmanager + def profile_operation(self, operation_name: str, **tags): + """Context manager to profile an operation.""" + start_time = time.time() + start_memory = psutil.Process().memory_info().rss + + try: + yield + success = True + except Exception as e: + success = False + logger.error(f"Operation {operation_name} failed: {e}") + raise + finally: + end_time = time.time() + end_memory = psutil.Process().memory_info().rss + + duration = end_time - start_time + memory_delta = (end_memory - start_memory) / 1024 / 1024 # MB + + # Record metrics + operation_tags = {"operation": operation_name, "success": str(success), **tags} + self.metrics.record_metric("operation_duration_seconds", duration, operation_tags) + self.metrics.record_metric("operation_memory_delta_mb", memory_delta, operation_tags) + + # Update operation stats + self.operation_stats[operation_name].append({ + "duration": duration, + "memory_delta": memory_delta, + "success": success, + "timestamp": datetime.now() + }) + + def get_operation_summary(self, operation_name: str = None) -> Dict[str, Any]: + """Get summary of operation performance.""" + if operation_name: + operations_to_analyze = {operation_name: self.operation_stats[operation_name]} + else: + operations_to_analyze = dict(self.operation_stats) + + summary = {} + + for op_name, stats in operations_to_analyze.items(): + if not stats: + continue + + durations = [s["duration"] for s in stats] + memory_deltas = [s["memory_delta"] for s in stats] + success_rate = sum(1 for s in stats if s["success"]) / len(stats) + + summary[op_name] = { + "total_calls": len(stats), + "success_rate": success_rate, + "avg_duration_seconds": sum(durations) / len(durations), + "avg_memory_delta_mb": sum(memory_deltas) / len(memory_deltas), + "min_duration": min(durations), + "max_duration": max(durations) + } + + return summary + +# Global instances +metrics_collector = MetricsCollector() +performance_profiler = PerformanceProfiler(metrics_collector) + +# Convenience decorators +def track_performance(operation_name: str = None): + """Decorator to automatically track function performance.""" + def decorator(func): + nonlocal operation_name + if operation_name is None: + operation_name = f"{func.__module__}.{func.__name__}" + + def wrapper(*args, **kwargs): + with performance_profiler.profile_operation(operation_name): + result = func(*args, **kwargs) + metrics_collector.increment_counter(f"{operation_name}_calls") + return result + return wrapper + return decorator + +def track_api_call(service_name: str): + """Decorator specifically for tracking API calls.""" + def decorator(func): + def wrapper(*args, **kwargs): + with performance_profiler.profile_operation("api_call", service=service_name): + try: + result = func(*args, **kwargs) + metrics_collector.increment_counter("api_calls_success", tags={"service": service_name}) + return result + except Exception: + metrics_collector.increment_counter("api_calls_failed", tags={"service": service_name}) + raise + return wrapper + return decorator diff --git a/mcp_hub/reliability_utils.py b/mcp_hub/reliability_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..bbad3a7c4ae9896adf9e54c101de984f0ac0e9ea --- /dev/null +++ b/mcp_hub/reliability_utils.py @@ -0,0 +1,254 @@ +"""Rate limiting and circuit breaker patterns for robust API interactions.""" + +import time +from datetime import datetime +from typing import Callable, Any, Dict +from functools import wraps +from threading import Lock +from collections import deque +from .exceptions import APIError +from .logging_config import logger + +class RateLimiter: + """Token bucket rate limiter for API calls.""" + + def __init__(self, calls_per_second: float = 1.0, burst_size: int = 5): + """ + Initialize rate limiter. + + Args: + calls_per_second: Maximum calls per second + burst_size: Maximum burst of calls allowed + """ + self.calls_per_second = calls_per_second + self.burst_size = float(burst_size) + self.tokens = float(burst_size) + self.last_update = time.time() + self.lock = Lock() + + def acquire(self, timeout: float = None) -> bool: + """ + Acquire a token for making an API call. + + Args: + timeout: Maximum time to wait for a token + + Returns: + True if token acquired, False if timeout + """ + start_time = time.time() + + while True: + with self.lock: + now = time.time() + # Add tokens based on elapsed time + time_passed = now - self.last_update + self.tokens = min( + self.burst_size, + self.tokens + time_passed * self.calls_per_second + ) + self.last_update = now + + if self.tokens >= 1: + self.tokens -= 1 + return True + + # Check timeout + if timeout and (time.time() - start_time) >= timeout: + return False + + # Wait before retrying + time.sleep(0.1) + +class CircuitBreaker: + """Circuit breaker pattern for handling API failures gracefully.""" + + def __init__( + self, + failure_threshold: int = 5, + timeout: int = 60, + expected_exception: type = Exception + ): + """ + Initialize circuit breaker. + + Args: + failure_threshold: Number of failures before opening circuit + timeout: Seconds to wait before trying again + expected_exception: Exception type that triggers circuit breaker + """ + self.failure_threshold = failure_threshold + self.timeout = timeout + self.expected_exception = expected_exception + + self.failure_count = 0 + self.last_failure_time = None + self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN + self.lock = Lock() + + def _can_attempt(self) -> bool: + """Check if we can attempt the operation.""" + if self.state == "CLOSED": + return True + elif self.state == "OPEN": + if (datetime.now() - self.last_failure_time).seconds >= self.timeout: + self.state = "HALF_OPEN" + return True + return False + else: # HALF_OPEN + return True + + def _record_success(self): + """Record a successful operation.""" + self.failure_count = 0 + self.state = "CLOSED" + + def _record_failure(self): + """Record a failed operation.""" + self.failure_count += 1 + self.last_failure_time = datetime.now() + + if self.failure_count >= self.failure_threshold: + self.state = "OPEN" + logger.warning(f"Circuit breaker opened after {self.failure_count} failures") + + def call(self, func: Callable, *args, **kwargs) -> Any: + """ + Execute function with circuit breaker protection. + + Args: + func: Function to execute + *args, **kwargs: Arguments for the function + + Returns: + Function result + + Raises: + APIError: If circuit is open or function fails + """ + with self.lock: + if not self._can_attempt(): + raise APIError( + "CircuitBreaker", + f"Circuit breaker is OPEN. Last failure: {self.last_failure_time}" + ) + + try: + result = func(*args, **kwargs) + with self.lock: + self._record_success() + return result + + except self.expected_exception as e: + with self.lock: + self._record_failure() + logger.error(f"Circuit breaker recorded failure: {str(e)}") + raise APIError("CircuitBreaker", f"Protected function failed: {str(e)}") + +# Global instances for different services +nebius_rate_limiter = RateLimiter(calls_per_second=2.0, burst_size=5) +tavily_rate_limiter = RateLimiter(calls_per_second=1.0, burst_size=3) + +nebius_circuit_breaker = CircuitBreaker(failure_threshold=3, timeout=30) +tavily_circuit_breaker = CircuitBreaker(failure_threshold=3, timeout=30) + +def rate_limited(service: str = "default", timeout: float = 10.0): + """ + Decorator to rate limit function calls. + + Args: + service: Service name (nebius, tavily, or default) + timeout: Maximum time to wait for rate limit token + """ + def decorator(func: Callable): + @wraps(func) + def wrapper(*args, **kwargs): + # Select appropriate rate limiter + if service == "nebius": + limiter = nebius_rate_limiter + elif service == "tavily": + limiter = tavily_rate_limiter + else: + limiter = RateLimiter() # Default limiter + + if not limiter.acquire(timeout=timeout): + raise APIError(service, f"Rate limit timeout after {timeout}s") + + return func(*args, **kwargs) + return wrapper + return decorator + +def circuit_protected(service: str = "default"): + """ + Decorator to protect function calls with circuit breaker. + + Args: + service: Service name (nebius, tavily, or default) + """ + def decorator(func: Callable): + @wraps(func) + def wrapper(*args, **kwargs): + # Select appropriate circuit breaker + if service == "nebius": + breaker = nebius_circuit_breaker + elif service == "tavily": + breaker = tavily_circuit_breaker + else: + breaker = CircuitBreaker() # Default breaker + + return breaker.call(func, *args, **kwargs) + return wrapper + return decorator + +class APIHealthMonitor: + """Monitor API health and performance metrics.""" + + def __init__(self, window_size: int = 100): + """ + Initialize health monitor. + + Args: + window_size: Number of recent calls to track + """ + self.window_size = window_size + self.call_history = deque(maxlen=window_size) + self.lock = Lock() + + def record_call(self, service: str, success: bool, response_time: float): + """Record an API call result.""" + with self.lock: + self.call_history.append({ + "service": service, + "success": success, + "response_time": response_time, + "timestamp": datetime.now() + }) + + def get_health_stats(self, service: str = None) -> Dict[str, Any]: + """Get health statistics for a service or all services.""" + with self.lock: + if service: + calls = [call for call in self.call_history if call["service"] == service] + else: + calls = list(self.call_history) + + if not calls: + return {"error": "No call history available"} + + total_calls = len(calls) + successful_calls = sum(1 for call in calls if call["success"]) + success_rate = successful_calls / total_calls + + response_times = [call["response_time"] for call in calls] + avg_response_time = sum(response_times) / len(response_times) + + return { + "service": service or "all", + "total_calls": total_calls, + "success_rate": success_rate, + "avg_response_time_ms": avg_response_time * 1000, + "recent_failures": total_calls - successful_calls + } + +# Global health monitor +health_monitor = APIHealthMonitor() diff --git a/mcp_hub/sandbox_pool.py b/mcp_hub/sandbox_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..bb35b805349aeb4d39c772f93682038ca32847e9 --- /dev/null +++ b/mcp_hub/sandbox_pool.py @@ -0,0 +1,701 @@ +""" +Warm Sandbox Pool for Modal - Async Queue-Based Implementation +This module provides a pre-warmed pool of Modal sandboxes to reduce cold-start latency. +""" +import asyncio +import time +from typing import Optional, Dict, Any +from contextlib import asynccontextmanager +from dataclasses import dataclass +from enum import Enum + +import modal + +from mcp_hub.logging_config import logger +from mcp_hub.exceptions import CodeExecutionError + + +class SandboxHealth(Enum): + """Sandbox health status.""" + HEALTHY = "healthy" + UNHEALTHY = "unhealthy" + UNKNOWN = "unknown" + + +@dataclass +class PooledSandbox: + """Container for a pooled sandbox with metadata.""" + sandbox: modal.Sandbox + created_at: float + last_used: float + health: SandboxHealth = SandboxHealth.UNKNOWN + use_count: int = 0 + + +class WarmSandboxPool: + """Async queue-based warm sandbox pool with health checking.""" + + def __init__( + self, + app: modal.App, + image: modal.Image, + pool_size: int = 3, + max_age_seconds: int = 300, # 5 minutes + max_uses_per_sandbox: int = 10, + health_check_interval: int = 60, # 1 minute + ): + self.app = app + self.image = image + self.pool_size = pool_size + self.max_age_seconds = max_age_seconds + self.max_uses_per_sandbox = max_uses_per_sandbox + self.health_check_interval = health_check_interval + + # Queue to hold available sandboxes + self._sandbox_queue: asyncio.Queue[PooledSandbox] = asyncio.Queue(maxsize=pool_size) + + # Background tasks + self._warmup_task: Optional[asyncio.Task] = None + self._health_check_task: Optional[asyncio.Task] = None + self._cleanup_task: Optional[asyncio.Task] = None + + # Pool statistics + self._stats = { + "created": 0, + "reused": 0, + "recycled": 0, + "health_checks": 0, + "failures": 0 + } + + # Health tracking for better error recovery + self._consecutive_failures = 0 + self._last_successful_creation = time.time() + self._pool_reset_threshold = 5 # Reset pool after 5 consecutive failures + + self._running = False + + async def start(self): + """Start the pool and background tasks.""" + if self._running: + return + + self._running = True + logger.info(f"Starting warm sandbox pool with {self.pool_size} sandboxes") + + # Start background tasks + self._warmup_task = asyncio.create_task(self._warmup_pool()) + self._health_check_task = asyncio.create_task(self._health_check_loop()) + self._cleanup_task = asyncio.create_task(self._cleanup_loop()) + + # Wait for initial warmup + await asyncio.sleep(1) # Give warmup a moment to start + + async def stop(self): + """Stop the pool and cleanup resources.""" + if not self._running: + return + + self._running = False + logger.info("Stopping warm sandbox pool") + + # Cancel background tasks + for task in [self._warmup_task, self._health_check_task, self._cleanup_task]: + if task and not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + # Cleanup remaining sandboxes + while not self._sandbox_queue.empty(): + try: + pooled_sb = self._sandbox_queue.get_nowait() + await self._terminate_sandbox(pooled_sb.sandbox) + except asyncio.QueueEmpty: + break + + @asynccontextmanager + async def get_sandbox(self, timeout: float = 5.0): + pooled_sb = None + created_new = False + try: + # Check if we need to reset the pool due to consecutive failures + if self._consecutive_failures >= self._pool_reset_threshold: + logger.warning(f"Pool has {self._consecutive_failures} consecutive failures, attempting reset") + await self._emergency_pool_reset() + + # Try to get a warm sandbox from the pool, retry if not alive + max_retries = 3 # Increased retries for better reliability + for attempt in range(max_retries): + try: + # Try to get from pool first + pooled_sb = await asyncio.wait_for(self._sandbox_queue.get(), timeout=timeout) + # Check if the sandbox is alive + alive = await self._is_sandbox_alive(pooled_sb.sandbox) + if not alive: + logger.info(f"Got dead sandbox from pool on attempt {attempt + 1}, terminating and trying next.") + await self._terminate_sandbox(pooled_sb.sandbox) + pooled_sb = None + continue # Try again + + # Sandbox is alive, use it + pooled_sb.last_used = time.time() + pooled_sb.use_count += 1 + self._stats["reused"] += 1 + self._consecutive_failures = 0 # Reset failure counter on success + break + + except asyncio.TimeoutError: + # Pool empty or taking too long, create a new one + logger.info(f"Pool timeout on attempt {attempt + 1}, creating new sandbox") + try: + sandbox = await self._create_sandbox() + pooled_sb = PooledSandbox( + sandbox=sandbox, + created_at=time.time(), + last_used=time.time(), + use_count=1 + ) + created_new = True + self._stats["created"] += 1 + self._consecutive_failures = 0 # Reset failure counter on success + self._last_successful_creation = time.time() + break + except Exception as create_error: + logger.error(f"Failed to create sandbox on attempt {attempt + 1}: {create_error}") + self._consecutive_failures += 1 + if attempt == max_retries - 1: # Last attempt + raise CodeExecutionError(f"Failed to create sandbox after {max_retries} attempts: {create_error}") + await asyncio.sleep(2 ** attempt) # Exponential backoff + else: + self._consecutive_failures += 1 + raise CodeExecutionError("Could not obtain a live sandbox from the pool after all retry attempts.") + + logger.info(f"Yielding sandbox of type from sandbox_pool: {type(pooled_sb.sandbox)}") + yield pooled_sb.sandbox + + except Exception as e: + logger.error(f"Error getting sandbox: {e}") + self._stats["failures"] += 1 + self._consecutive_failures += 1 + raise CodeExecutionError(f"Failed to get sandbox: {e}") + finally: + if pooled_sb: + should_recycle = ( + not created_new and + self._should_recycle_sandbox(pooled_sb) and + self._running + ) + if should_recycle: + # Double-check sandbox is alive and functional before returning to pool + if await self._is_sandbox_alive(pooled_sb.sandbox): + # Additional check: try a quick execution to ensure sandbox is fully functional + try: + await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: pooled_sb.sandbox.exec("python", "-c", "import sys; print('ready')", timeout=2) + ), + timeout=3.0 + ) + + # Sandbox is healthy and functional - return to pool + try: + self._sandbox_queue.put_nowait(pooled_sb) + logger.debug("Returned healthy sandbox to pool") + except asyncio.QueueFull: + # Pool is full - terminate excess sandbox + await self._terminate_sandbox(pooled_sb.sandbox) + logger.debug("Pool full, terminated excess sandbox") + except Exception as e: + # Sandbox failed functional test - terminate it + logger.debug(f"Sandbox failed functional test, terminating: {e}") + await self._terminate_sandbox(pooled_sb.sandbox) + else: + # Sandbox is dead - terminate it + logger.debug("Sandbox is dead, terminating instead of recycling") + await self._terminate_sandbox(pooled_sb.sandbox) + else: + # Should not recycle - terminate sandbox + await self._terminate_sandbox(pooled_sb.sandbox) + if not created_new: + self._stats["recycled"] += 1 + logger.debug("Terminated sandbox (exceeded recycle criteria)") + + async def _create_sandbox(self) -> modal.Sandbox: + """Create a new Modal sandbox with timeout protection.""" + try: + # Add timeout protection for sandbox creation + sandbox_creation = asyncio.get_event_loop().run_in_executor( + None, + lambda: modal.Sandbox.create( + app=self.app, + image=self.image, + cpu=2.0, + memory=1024, + timeout=35 + ) + ) + # Wait for sandbox creation with timeout + sandbox = await asyncio.wait_for(sandbox_creation, timeout=120) # 2 minute timeout + logger.debug(f"Created new sandbox of type: {type(sandbox)}") + return sandbox + except asyncio.TimeoutError: + logger.error("Sandbox creation timed out after 2 minutes") + raise Exception("Sandbox creation timed out - Modal may be experiencing issues") + except Exception as e: + logger.error(f"Failed to create sandbox: {e}") + raise + + async def _terminate_sandbox(self, sandbox: modal.Sandbox): + """Safely terminate a sandbox with better error handling.""" + try: + # Check if sandbox is still responsive before termination + if hasattr(sandbox, '_terminated') and sandbox._terminated: + logger.debug("Sandbox already terminated") + return + + # Use asyncio timeout for termination + await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor(None, sandbox.terminate), + timeout=10.0 # 10 second timeout for termination + ) + logger.debug("Terminated sandbox successfully") + except asyncio.TimeoutError: + logger.warning("Sandbox termination timed out - may be unresponsive") + except Exception as e: + # Log the error but don't fail - sandbox may already be dead + logger.warning(f"Failed to terminate sandbox (may already be dead): {e}") + # Mark sandbox as terminated to avoid repeated attempts + if hasattr(sandbox, '_terminated'): + sandbox._terminated = True + + def _should_recycle_sandbox(self, pooled_sb: PooledSandbox) -> bool: + """Determine if a sandbox should be recycled back to the pool.""" + now = time.time() + + # Check age + if now - pooled_sb.created_at > self.max_age_seconds: + logger.debug("Sandbox too old, not recycling") + return False + + # Check usage count + if pooled_sb.use_count >= self.max_uses_per_sandbox: + logger.debug("Sandbox used too many times, not recycling") + return False + + # Check health (if we've checked it) + if pooled_sb.health == SandboxHealth.UNHEALTHY: + logger.debug("Sandbox unhealthy, not recycling") + return False + + return True + async def _warmup_pool(self): + """Background task to maintain warm sandboxes in the pool with aggressive replenishment.""" + while self._running: + try: + current_size = self._sandbox_queue.qsize() + + # More aggressive warmup - start warming when below 90% capacity + warmup_threshold = max(1, int(self.pool_size * 0.9)) + + if current_size < warmup_threshold: + needed = self.pool_size - current_size + logger.info(f"Pool size ({current_size}) below threshold ({warmup_threshold}). Warming {needed} sandboxes...") + + # Create new sandboxes to fill the pool - but limit concurrent creation + max_concurrent = min(needed, 2) # Don't overwhelm Modal + tasks = [] + for _ in range(max_concurrent): + task = asyncio.create_task(self._create_and_queue_sandbox()) + tasks.append(task) + + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + # Log any failures + successful = 0 + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.warning(f"Failed to create sandbox {i+1}/{max_concurrent}: {result}") + else: + successful += 1 + + if successful > 0: + logger.info(f"Successfully warmed {successful}/{max_concurrent} sandboxes") + + # Adaptive sleep interval based on pool health + if current_size == 0: + # Critical: no sandboxes available + sleep_interval = 1 + elif current_size < warmup_threshold: + # Low: need more sandboxes + sleep_interval = 2 + else: + # Healthy: normal monitoring + sleep_interval = 5 + + await asyncio.sleep(sleep_interval) + + except Exception as e: + logger.error(f"Error in warmup loop: {e}") + await asyncio.sleep(10) # Wait longer on error + + async def _create_and_queue_sandbox(self): + """Create a sandbox and add it to the queue.""" + start_time = time.time() + try: + # Create the sandbox + sandbox = await self._create_sandbox() + creation_time = time.time() - start_time + logger.info(f"Sandbox creation took {creation_time:.2f}s") + + # Proactively warm up the sandbox with core imports + warmup_start = time.time() + await self._warmup_sandbox_imports(sandbox) + warmup_time = time.time() - warmup_start + logger.info(f"Sandbox warmup with imports took {warmup_time:.2f}s") + + pooled_sb = PooledSandbox( + sandbox=sandbox, + created_at=time.time(), + last_used=time.time() + ) + + try: + self._sandbox_queue.put_nowait(pooled_sb) + total_time = time.time() - start_time + logger.info(f"Added warm sandbox to pool (total time: {total_time:.2f}s)") + except asyncio.QueueFull: + # Pool is full, terminate this sandbox + await self._terminate_sandbox(sandbox) + + except Exception as e: + total_time = time.time() - start_time + logger.error(f"Failed to create and queue sandbox after {total_time:.2f}s: {e}") + + async def _warmup_sandbox_imports(self, sandbox: modal.Sandbox): + """Warm up sandbox by importing core packages.""" + try: + from mcp_hub.package_utils import get_warmup_import_commands + + # Get warmup commands + import_commands = get_warmup_import_commands() + warmup_script = "; ".join(import_commands) + + # Execute the warmup script + logger.debug("Running sandbox warmup imports...") + proc = await asyncio.get_event_loop().run_in_executor( + None, + lambda: sandbox.exec("python", "-c", warmup_script, timeout=30) + ) + + # Check if warmup was successful + if hasattr(proc, 'stdout') and hasattr(proc.stdout, 'read'): + output = proc.stdout.read() + if "Core packages warmed up successfully" in output: + logger.debug("Sandbox warmup imports completed successfully") + else: + logger.warning(f"Sandbox warmup completed but output unexpected: {output}") + else: + logger.debug("Sandbox warmup imports completed") + + except Exception as e: + logger.warning(f"Failed to warm up sandbox imports (sandbox still usable): {e}") + async def _health_check_loop(self): + """Background task to check sandbox health and perform proactive cleanup.""" + while self._running: + try: + # Perform regular health checks every interval + await asyncio.sleep(self.health_check_interval) + + # First do a quick proactive cleanup + cleaned = await self._proactive_cleanup() + + # Then do the full health check + await self._perform_health_checks() + + # If we cleaned up sandboxes, trigger warmup + if cleaned > 0: + logger.info(f"Health check cleaned {cleaned} sandboxes, pool may need warming") + + except Exception as e: + logger.error(f"Error in health check loop: {e}") + await asyncio.sleep(10) # Wait longer on error + + async def _perform_health_checks(self): + """Perform health checks on sandboxes in the pool.""" + # This is a simplified health check - in practice you might want + # to run a simple command to verify the sandbox is responsive + temp_sandboxes = [] + + # Drain the queue to check each sandbox + while not self._sandbox_queue.empty(): + try: + pooled_sb = self._sandbox_queue.get_nowait() + is_healthy = await self._check_sandbox_health(pooled_sb.sandbox) + pooled_sb.health = SandboxHealth.HEALTHY if is_healthy else SandboxHealth.UNHEALTHY + if is_healthy: + temp_sandboxes.append(pooled_sb) + else: + # TERMINATE unhealthy sandbox + await self._terminate_sandbox(pooled_sb.sandbox) + self._stats["recycled"] += 1 + except asyncio.QueueEmpty: + break + + # Put healthy sandboxes back + for pooled_sb in temp_sandboxes: + try: + self._sandbox_queue.put_nowait(pooled_sb) + except asyncio.QueueFull: + await self._terminate_sandbox(pooled_sb.sandbox) + + self._stats["health_checks"] += 1 + logger.debug(f"Health check completed. Pool size: {self._sandbox_queue.qsize()}") + + async def _check_sandbox_health(self, sandbox: modal.Sandbox) -> bool: + """Check if a sandbox is healthy.""" + try: + # Run a simple Python command to check if the sandbox is responsive + proc = await asyncio.get_event_loop().run_in_executor( + None, + lambda: sandbox.exec("python", "-c", "print('health_check')", timeout=5) + ) + output = proc.stdout.read() + return "health_check" in output + except Exception as e: + logger.debug(f"Sandbox health check failed: {e}") + return False + + async def _cleanup_loop(self): + """Background task to cleanup old sandboxes.""" + while self._running: + try: + await asyncio.sleep(30) # Check every 30 seconds + await self._cleanup_old_sandboxes() + except Exception as e: + logger.error(f"Error in cleanup loop: {e}") + + async def _cleanup_old_sandboxes(self): + """Remove old sandboxes from the pool.""" + now = time.time() + temp_sandboxes = [] + + while not self._sandbox_queue.empty(): + try: + pooled_sb = self._sandbox_queue.get_nowait() + if now - pooled_sb.created_at < self.max_age_seconds: + temp_sandboxes.append(pooled_sb) + else: + # TERMINATE expired sandbox + await self._terminate_sandbox(pooled_sb.sandbox) + self._stats["recycled"] += 1 + logger.debug("Cleaned up old sandbox") + except asyncio.QueueEmpty: + break + + # Put non-expired sandboxes back + for pooled_sb in temp_sandboxes: + try: + self._sandbox_queue.put_nowait(pooled_sb) + except asyncio.QueueFull: + await self._terminate_sandbox(pooled_sb.sandbox) + + async def _is_sandbox_alive(self, sandbox: modal.Sandbox) -> bool: + """Check if a sandbox is alive by running a trivial command with better error handling.""" + try: + # Check if sandbox was already marked as terminated + if hasattr(sandbox, '_terminated') and sandbox._terminated: + return False + + # Use a shorter timeout for liveness checks + proc = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: sandbox.exec("python", "-c", "print('ping')", timeout=3) + ), + timeout=5.0 # Overall timeout + ) + + if hasattr(proc, "stdout") and hasattr(proc.stdout, "read"): + out = proc.stdout.read() + return "ping" in out + else: + # For some Modal versions, output might be returned directly + out = str(proc) + return "ping" in out + + except asyncio.TimeoutError: + logger.debug("Liveness check timed out - sandbox likely dead") + return False + except Exception as e: + logger.debug(f"Liveness check failed: {e}") + # Mark sandbox as dead to avoid repeated checks + if hasattr(sandbox, '_terminated'): + sandbox._terminated = True + return False + + async def _emergency_pool_reset(self): + """Emergency reset of the pool when too many consecutive failures occur.""" + logger.warning("Performing emergency pool reset due to consecutive failures") + + # Drain and terminate all sandboxes in the pool + terminated_count = 0 + while not self._sandbox_queue.empty(): + try: + pooled_sb = self._sandbox_queue.get_nowait() + await self._terminate_sandbox(pooled_sb.sandbox) + terminated_count += 1 + except asyncio.QueueEmpty: + break + + logger.info(f"Emergency reset: terminated {terminated_count} sandboxes") + + # Reset failure counter + self._consecutive_failures = 0 + + # Try to create one fresh sandbox to test if the underlying issue is resolved + try: + test_sandbox = await self._create_sandbox() + test_pooled = PooledSandbox( + sandbox=test_sandbox, + created_at=time.time(), + last_used=time.time(), + use_count=0 + ) + self._sandbox_queue.put_nowait(test_pooled) + logger.info("Emergency reset successful: created test sandbox") + except Exception as e: + logger.error(f"Emergency reset failed to create test sandbox: {e}") + # Still reset the counter to allow retries + pass + + def get_stats(self) -> Dict[str, Any]: + """Get pool statistics including health metrics.""" + return { + **self._stats, + "pool_size": self._sandbox_queue.qsize(), + "target_pool_size": self.pool_size, + "running": self._running, + "consecutive_failures": self._consecutive_failures, + "last_successful_creation": self._last_successful_creation, + "time_since_last_success": time.time() - self._last_successful_creation, + "health_status": "healthy" if self._consecutive_failures < 3 else "degraded" if self._consecutive_failures < self._pool_reset_threshold else "critical" + } + + async def _proactive_cleanup(self): + """Proactively clean up dead or unhealthy sandboxes from the pool.""" + temp_sandboxes = [] + cleaned_count = 0 + + # Drain the queue to check each sandbox + while not self._sandbox_queue.empty(): + try: + pooled_sb = self._sandbox_queue.get_nowait() + + # Quick health check + if await self._is_sandbox_alive(pooled_sb.sandbox): + # Sandbox is alive - keep it + temp_sandboxes.append(pooled_sb) + else: + # Sandbox is dead - terminate it + await self._terminate_sandbox(pooled_sb.sandbox) + cleaned_count += 1 + logger.debug("Cleaned up dead sandbox during proactive cleanup") + + except asyncio.QueueEmpty: + break + + # Put healthy sandboxes back + for pooled_sb in temp_sandboxes: + try: + self._sandbox_queue.put_nowait(pooled_sb) + except asyncio.QueueFull: + # Shouldn't happen, but terminate if it does + await self._terminate_sandbox(pooled_sb.sandbox) + cleaned_count += 1 + + if cleaned_count > 0: + logger.info(f"Proactive cleanup removed {cleaned_count} dead sandboxes") + + return cleaned_count + +# Helper function for testing and debugging the sandbox pool +async def test_sandbox_pool_health(pool: WarmSandboxPool) -> Dict[str, Any]: + """Test sandbox pool health and return detailed diagnostics.""" + diagnostics: Dict[str, Any] = { + "timestamp": time.time(), + "pool_stats": pool.get_stats(), + "tests": {} + } + + logger.info("Starting sandbox pool health test...") + + # Test 1: Pool basic stats + stats = pool.get_stats() + diagnostics["tests"]["pool_stats"] = { + "passed": True, + "details": stats + } + + # Test 2: Try to get a sandbox + try: + async with pool.get_sandbox(timeout=10.0) as sandbox: + # Test 3: Try to run a simple command + try: + proc = await asyncio.get_event_loop().run_in_executor( + None, + lambda: sandbox.exec("python", "-c", "print('health_test_ok')", timeout=5) + ) + output = proc.stdout.read() if hasattr(proc.stdout, "read") else str(proc) + + diagnostics["tests"]["sandbox_execution"] = { + "passed": "health_test_ok" in output, + "output": output[:200], # First 200 chars + "details": "Successfully executed test command" + } + except Exception as e: + diagnostics["tests"]["sandbox_execution"] = { + "passed": False, + "error": str(e), + "details": "Failed to execute test command in sandbox" + } + + diagnostics["tests"]["sandbox_acquisition"] = { + "passed": True, + "details": "Successfully acquired and released sandbox" + } + + except Exception as e: + diagnostics["tests"]["sandbox_acquisition"] = { + "passed": False, + "error": str(e), + "details": "Failed to acquire sandbox from pool" + } + + diagnostics["tests"]["sandbox_execution"] = { + "passed": False, + "error": "Could not test - no sandbox available", + "details": "Skipped due to sandbox acquisition failure" + } + + # Test 4: Check pool warmup status + if pool._running: + warmup_needed = pool.pool_size - stats["pool_size"] + diagnostics["tests"]["pool_warmup"] = { + "passed": warmup_needed <= 1, # Allow 1 sandbox to be missing + "details": f"Pool has {stats['pool_size']}/{pool.pool_size} sandboxes, {warmup_needed} needed" + } + else: + diagnostics["tests"]["pool_warmup"] = { + "passed": False, + "details": "Pool is not running" + } + + # Overall health assessment + all_tests_passed = all(test.get("passed", False) for test in diagnostics["tests"].values()) + diagnostics["overall_health"] = "healthy" if all_tests_passed else "unhealthy" + + logger.info(f"Sandbox pool health test completed. Overall health: {diagnostics['overall_health']}") + return diagnostics diff --git a/mcp_hub/utils.py b/mcp_hub/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8c7a37f00e5ba70cc8c5eb3cbffe0e3afde6af8c --- /dev/null +++ b/mcp_hub/utils.py @@ -0,0 +1,439 @@ +"""Utility functions for the MCP Hub project.""" + +import json +import re +from typing import Dict, Any, List, Optional, Union +from openai import OpenAI, AsyncOpenAI +from .config import api_config, model_config +from .exceptions import APIError, ValidationError +from .logging_config import logger +import aiohttp +from huggingface_hub import InferenceClient + + +def create_nebius_client() -> OpenAI: + """Create and return a Nebius OpenAI client.""" + return OpenAI( + base_url=api_config.nebius_base_url, + api_key=api_config.nebius_api_key, + ) + +def create_async_nebius_client() -> AsyncOpenAI: + """Create and return an async Nebius OpenAI client.""" + return AsyncOpenAI( + base_url=api_config.nebius_base_url, + api_key=api_config.nebius_api_key, + ) + +def create_llm_client() -> Union[OpenAI, object]: + """Create and return an LLM client based on the configured provider.""" + if api_config.llm_provider == "nebius": + return create_nebius_client() + elif api_config.llm_provider == "openai": + return OpenAI(api_key=api_config.openai_api_key) + elif api_config.llm_provider == "anthropic": + try: + import anthropic + return anthropic.Anthropic(api_key=api_config.anthropic_api_key) + except ImportError: + raise APIError("Anthropic", "anthropic package not installed. Install with: pip install anthropic") + elif api_config.llm_provider == "huggingface": + # Try different HuggingFace client configurations for better compatibility + try: + # First try with hf-inference provider (most recent approach) + return InferenceClient( + provider="hf-inference", + api_key=api_config.huggingface_api_key, + ) + except Exception: + # Fallback to token-based authentication + return InferenceClient( + token=api_config.huggingface_api_key, + ) + else: + raise APIError("Config", f"Unsupported LLM provider: {api_config.llm_provider}") + +def create_async_llm_client() -> Union[AsyncOpenAI, object]: + """Create and return an async LLM client based on the configured provider.""" + if api_config.llm_provider == "nebius": + return create_async_nebius_client() + elif api_config.llm_provider == "openai": + return AsyncOpenAI(api_key=api_config.openai_api_key) + elif api_config.llm_provider == "anthropic": + try: + import anthropic + return anthropic.AsyncAnthropic(api_key=api_config.anthropic_api_key) + except ImportError: + raise APIError("Anthropic", "anthropic package not installed. Install with: pip install anthropic") + elif api_config.llm_provider == "huggingface": + # Try different HuggingFace client configurations for better compatibility + try: + # First try with hf-inference provider (most recent approach) + return InferenceClient( + provider="hf-inference", + api_key=api_config.huggingface_api_key, + ) + except Exception: + # Fallback to token-based authentication + return InferenceClient( + token=api_config.huggingface_api_key, + ) + else: + raise APIError("Config", f"Unsupported LLM provider: {api_config.llm_provider}") + +def validate_non_empty_string(value: str, field_name: str) -> None: + """Validate that a string is not empty or None.""" + if not value or not value.strip(): + raise ValidationError(f"{field_name} cannot be empty.") + +def extract_json_from_text(text: str) -> Dict[str, Any]: + """Extract JSON object from text that may contain markdown fences.""" + # Remove markdown code fences if present + if text.startswith("```"): + parts = text.split("```") + if len(parts) >= 3: + text = parts[1].strip() + else: + text = text.strip("```").strip() + + # Find JSON object boundaries + start_idx = text.find("{") + end_idx = text.rfind("}") + + if start_idx == -1 or end_idx == -1 or end_idx < start_idx: + raise ValidationError("Failed to locate JSON object in text.") + + json_candidate = text[start_idx:end_idx + 1] + + try: + return json.loads(json_candidate) + except json.JSONDecodeError as e: + raise ValidationError(f"Failed to parse JSON: {str(e)}") + +def extract_urls_from_text(text: str) -> List[str]: + """Extract URLs from text using regex.""" + url_pattern = r"(https?://[^\s]+)" + return re.findall(url_pattern, text) + +def make_nebius_completion( + model: str, + messages: List[Dict[str, str]], + temperature: float = 0.6, + response_format: Optional[Dict[str, Any]] = None +) -> str: + """Make a completion request to Nebius and return the content.""" + client = create_nebius_client() + + try: + kwargs = { + "model": model, + "messages": messages, + "temperature": temperature, + } + + if response_format: + kwargs["response_format"] = response_format + + completion = client.chat.completions.create(**kwargs) + return completion.choices[0].message.content.strip() + except Exception as e: + raise APIError("Nebius", str(e)) + +async def make_async_nebius_completion( + model: str, + messages: List[Dict[str, Any]], + temperature: float = 0.0, + response_format: Optional[Dict[str, Any]] = None, +) -> str: + """Make an async completion request to Nebius API.""" + try: + client = create_async_nebius_client() + + kwargs = { + "model": model, + "messages": messages, + "temperature": temperature + } + + if response_format: + kwargs["response_format"] = response_format + + response = await client.chat.completions.create(**kwargs) + + if not response.choices: + raise APIError("Nebius", "No completion choices returned") + + content = response.choices[0].message.content + if content is None: + raise APIError("Nebius", "Empty response content") + + return content.strip() + + except Exception as e: + if isinstance(e, APIError): + raise + raise APIError("Nebius", f"API call failed: {str(e)}") + +def make_llm_completion( + model: str, + messages: List[Dict[str, str]], + temperature: float = 0.6, + response_format: Optional[Dict[str, Any]] = None +) -> str: + """Make a completion request using the configured LLM provider.""" + provider = api_config.llm_provider + + try: + if provider == "nebius": + return make_nebius_completion(model, messages, temperature, response_format) + + elif provider == "openai": + client = create_llm_client() + kwargs = { + "model": model, + "messages": messages, + "temperature": temperature, + } + # OpenAI only supports simple response_format, not the extended Nebius format + if response_format and response_format.get("type") == "json_object": + kwargs["response_format"] = {"type": "json_object"} + completion = client.chat.completions.create(**kwargs) + return completion.choices[0].message.content.strip() + + elif provider == "anthropic": + client = create_llm_client() + # Convert OpenAI format to Anthropic format + anthropic_messages = [] + system_message = None + + for msg in messages: + if msg["role"] == "system": + system_message = msg["content"] + else: + anthropic_messages.append({ + "role": msg["role"], + "content": msg["content"] + }) + + kwargs = { + "model": model, + "messages": anthropic_messages, + "temperature": temperature, + "max_tokens": 1000, + } + if system_message: + kwargs["system"] = system_message + + response = client.messages.create(**kwargs) + return response.content[0].text.strip() + + elif provider == "huggingface": + # Try HuggingFace with fallback to Nebius + hf_error = None + try: + client = create_llm_client() + + # Try multiple HuggingFace API approaches + + # Method 1: Try chat.completions.create (OpenAI-compatible) + try: + response = client.chat.completions.create( + model=model, + messages=messages, + temperature=temperature, + max_tokens=1000, + ) + + # Extract the response content + if hasattr(response, 'choices') and response.choices: + return response.choices[0].message.content.strip() + else: + return str(response).strip() + + except Exception as e1: + hf_error = e1 + + # Method 2: Try chat_completion method (HuggingFace native) + try: + response = client.chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=1000, + ) + + # Handle different response formats + if hasattr(response, 'generated_text'): + return response.generated_text.strip() + elif isinstance(response, dict) and 'generated_text' in response: + return response['generated_text'].strip() + elif isinstance(response, list) and len(response) > 0: + if isinstance(response[0], dict) and 'generated_text' in response[0]: + return response[0]['generated_text'].strip() + + return str(response).strip() + + except Exception as e2: + # Both HuggingFace methods failed + hf_error = f"Method 1: {str(e1)}. Method 2: {str(e2)}" + raise APIError("HuggingFace", f"All HuggingFace methods failed. {hf_error}") + + except Exception as e: + # HuggingFace failed, try fallback to Nebius + if hf_error is None: + hf_error = str(e) + logger.warning(f"HuggingFace API failed: {hf_error}, falling back to Nebius") + + try: + # Use Nebius model appropriate for the task + nebius_model = model_config.get_model_for_provider("question_enhancer", "nebius") + return make_nebius_completion(nebius_model, messages, temperature, response_format) + except Exception as nebius_error: + raise APIError("HuggingFace", f"HuggingFace failed: {hf_error}. Nebius fallback also failed: {str(nebius_error)}") + + else: + raise APIError("Config", f"Unsupported LLM provider: {provider}") + + except Exception as e: + raise APIError(provider.title(), f"Completion failed: {str(e)}") + + +async def make_async_llm_completion( + model: str, + messages: List[Dict[str, Any]], + temperature: float = 0.0, + response_format: Optional[Dict[str, Any]] = None, +) -> str: + """Make an async completion request using the configured LLM provider.""" + provider = api_config.llm_provider + + try: + if provider == "nebius": + return await make_async_nebius_completion(model, messages, temperature, response_format) + + elif provider == "openai": + client = create_async_llm_client() + kwargs = { + "model": model, + "messages": messages, + "temperature": temperature + } + if response_format and response_format.get("type") == "json_object": + kwargs["response_format"] = {"type": "json_object"} + + response = await client.chat.completions.create(**kwargs) + + if not response.choices: + raise APIError("OpenAI", "No completion choices returned") + + content = response.choices[0].message.content + if content is None: + raise APIError("OpenAI", "Empty response content") + + return content.strip() + + elif provider == "anthropic": + client = create_async_llm_client() + anthropic_messages = [] + system_message = None + + for msg in messages: + if msg["role"] == "system": + system_message = msg["content"] + else: + anthropic_messages.append({ + "role": msg["role"], + "content": msg["content"] + }) + + kwargs = { + "model": model, + "messages": anthropic_messages, + "temperature": temperature, + "max_tokens": 1000, + } + if system_message: + kwargs["system"] = system_message + + response = await client.messages.create(**kwargs) + return response.content[0].text.strip() + + elif provider == "huggingface": + # HuggingFace doesn't support async, fallback to Nebius + logger.warning("HuggingFace does not support async operations, falling back to Nebius") + + try: + # Use Nebius model appropriate for the task + nebius_model = model_config.get_model_for_provider("question_enhancer", "nebius") + return await make_async_nebius_completion(nebius_model, messages, temperature, response_format) + except Exception as nebius_error: + raise APIError("HuggingFace", f"HuggingFace async not supported. Nebius fallback failed: {str(nebius_error)}") + + else: + raise APIError("Config", f"Unsupported LLM provider: {provider}") + + except Exception as e: + raise APIError(provider.title(), f"Async completion failed: {str(e)}") + +async def async_tavily_search(query: str, max_results: int = 3) -> Dict[str, Any]: + """Perform async web search using Tavily API.""" + try: + async with aiohttp.ClientSession() as session: + url = "https://api.tavily.com/search" + headers = { + "Content-Type": "application/json" + } + data = { + "api_key": api_config.tavily_api_key, + "query": query, + "search_depth": "basic", + "max_results": max_results, + "include_answer": True + } + + async with session.post(url, headers=headers, json=data) as response: + if response.status != 200: + raise APIError("Tavily", f"HTTP {response.status}: {await response.text()}") + + result = await response.json() + return { + "query": result.get("query", query), + "tavily_answer": result.get("answer"), + "results": result.get("results", []), + "data_source": "Tavily Search API", + } + + except aiohttp.ClientError as e: + raise APIError("Tavily", f"HTTP request failed: {str(e)}") + except Exception as e: + if isinstance(e, APIError): + raise + raise APIError("Tavily", f"Search failed: {str(e)}") + +def format_search_results(results: List[Dict[str, Any]]) -> str: + """Format search results into a readable string.""" + if not results: + return "No search results found." + + snippets = [] + for idx, item in enumerate(results, 1): + title = item.get("title", "No Title") + url = item.get("url", "") + content = item.get("content", "") + + snippet = f"Result {idx}:\nTitle: {title}\nURL: {url}\nSnippet: {content}\n" + snippets.append(snippet) + + return "\n".join(snippets).strip() + +def create_apa_citation(url: str, year: str = None) -> str: + """Create a simple APA-style citation from a URL.""" + if not year: + year = api_config.current_year + + try: + domain = url.split("/")[2] + title = domain.replace("www.", "").split(".")[0].capitalize() + return f"{title}. ({year}). Retrieved from {url}" + except (IndexError, AttributeError): + return f"Unknown Source. ({year}). Retrieved from {url}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..5caab6670ac8e417034eae3df3bc25794b29a203 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "mcp-hub-project" +version = "0.2.0" +description = "Advanced MCP Hub with Inter-Agent Communication and Performance Monitoring" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "gradio-client>=1.10.2", + "gradio[mcp]>=5.33.0", + "modal>=1.0.2", + "openai>=1.84.0", + "tavily-python>=0.7.4", + "python-dotenv>=1.0.0", + "psutil>=5.9.0", + "aiohttp>=3.8.0", + "anthropic>=0.52.2", + "huggingface>=0.0.1", + "huggingface-hub>=0.32.4", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-cov>=4.1.0", + "black>=23.0.0", + "isort>=5.12.0", + "mypy>=1.5.0", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..485c86a3c3079b2d65c0a1ea2a1535418ef3e0fb --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +minversion = 6.0 +addopts = -ra --strict-markers --strict-config --cov=app --cov=mcp_hub --cov-report=term-missing --cov-report=html:htmlcov --cov-branch +testpaths = tests +markers = + unit: Unit tests + integration: Integration tests + async_test: Async test cases + slow: Slow running tests + requires_api: Tests that need API keys +asyncio_mode = auto \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd8cdcfdcdd4c1f81f673da19ce5df31b6ce0046 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +gradio-client>=1.10.2, +gradio[mcp]>=5.33.0, +modal>=1.0.2 +openai>=1.84.0 +tavily-python>=0.7.4 +python-dotenv>=1.0.0 +psutil>=5.9.0 +aiohttp>=3.8.0 +anthropic>=0.52.2 +huggingface>=0.0.1 +huggingface-hub>=0.32.4 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ab1ee3eb75d43172477bcc0ceffcd39ff91cc309 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for MCP Hub.""" \ No newline at end of file diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d285e89d7fab18fa4285cae259592624c5426ad0 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/conftest.cpython-312-pytest-8.4.0.pyc b/tests/__pycache__/conftest.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a9493dce3d56f53dda4df82c533c217e67003fb Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..e5490cb297caafd0f4aa20962c1417cb95470afd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,142 @@ +"""Common test fixtures and configuration.""" + +import pytest +import asyncio +import os +from unittest.mock import Mock, MagicMock, patch +from typing import Dict, Any, Generator + +# Mock environment variables for testing - set them globally before any imports +TEST_ENV_VARS = { + "TAVILY_API_KEY": "tvly-test-key-12345", + "NEBIUS_API_KEY": "test-nebius-key", + "OPENAI_API_KEY": "test-openai-key", + "ANTHROPIC_API_KEY": "test-anthropic-key", + "HUGGINGFACE_API_KEY": "test-hf-key", + "LLM_PROVIDER": "nebius" +} + +# Set environment variables immediately +for key, value in TEST_ENV_VARS.items(): + os.environ[key] = value + +@pytest.fixture +def mock_tavily_client(): + """Mock Tavily client for web search tests.""" + mock_client = Mock() + mock_client.search.return_value = { + "results": [ + { + "title": "Test Result 1", + "url": "https://example.com/1", + "content": "Test content 1", + "score": 0.9 + }, + { + "title": "Test Result 2", + "url": "https://example.com/2", + "content": "Test content 2", + "score": 0.8 + } + ], + "answer": "Test search summary" + } + return mock_client + +@pytest.fixture +def mock_llm_response(): + """Mock LLM completion response.""" + return '{"sub_questions": ["Question 1?", "Question 2?", "Question 3?"]}' + +@pytest.fixture +def mock_modal_sandbox(): + """Mock Modal sandbox for code execution tests.""" + mock_sandbox = Mock() + mock_sandbox.exec.return_value = Mock(stdout="Test output", stderr="", returncode=0) + return mock_sandbox + +@pytest.fixture +def sample_user_request(): + """Sample user request for testing.""" + return "Create a Python script to analyze CSV data and generate charts" + +@pytest.fixture +def sample_search_results(): + """Sample search results for testing.""" + return [ + { + "title": "Python Data Analysis Tutorial", + "url": "https://example.com/pandas-tutorial", + "content": "Learn how to analyze CSV data with pandas and matplotlib...", + "score": 0.95 + }, + { + "title": "Chart Generation with Python", + "url": "https://example.com/charts", + "content": "Create stunning charts and visualizations...", + "score": 0.87 + } + ] + +@pytest.fixture +def sample_code(): + """Sample Python code for testing.""" + return ''' +import pandas as pd +import matplotlib.pyplot as plt + +# Load data +df = pd.read_csv('data.csv') + +# Generate chart +df.plot(kind='bar') +plt.show() +''' + +@pytest.fixture +def mock_config(): + """Mock configuration objects.""" + api_config = Mock() + api_config.tavily_api_key = "tvly-test-key" + api_config.llm_provider = "nebius" + api_config.nebius_api_key = "test-nebius-key" + + model_config = Mock() + model_config.get_model_for_provider.return_value = "meta-llama/llama-3.1-8b-instruct" + + return api_config, model_config + +@pytest.fixture +def event_loop(): + """Create an event loop for async tests.""" + loop = asyncio.new_event_loop() + yield loop + loop.close() + +class MockAgent: + """Base mock agent class for testing.""" + def __init__(self, name: str): + self.name = name + self.call_count = 0 + + def __call__(self, *args, **kwargs): + self.call_count += 1 + return {"success": True, "agent": self.name, "calls": self.call_count} + +@pytest.fixture +def mock_agents(): + """Mock agent instances for orchestrator testing.""" + return { + "question_enhancer": MockAgent("question_enhancer"), + "web_search": MockAgent("web_search"), + "llm_processor": MockAgent("llm_processor"), + "citation_formatter": MockAgent("citation_formatter"), + "code_generator": MockAgent("code_generator"), + "code_runner": MockAgent("code_runner") + } + +@pytest.fixture +def disable_advanced_features(): + """Disable advanced features for basic testing.""" + with patch('app.ADVANCED_FEATURES_AVAILABLE', False): + yield \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d431cdf48ed87d12118cd42967c1f7768138f5e3 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests package.""" \ No newline at end of file diff --git a/tests/integration/__pycache__/__init__.cpython-312.pyc b/tests/integration/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2aefe4a03b5765f814e4adc0306940596dcb5a7b Binary files /dev/null and b/tests/integration/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/integration/__pycache__/test_async_sync_error_handling.cpython-312-pytest-8.4.0.pyc b/tests/integration/__pycache__/test_async_sync_error_handling.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01e4b20fc9940c51cfad66acbbeb56a16694ef9c Binary files /dev/null and b/tests/integration/__pycache__/test_async_sync_error_handling.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/integration/__pycache__/test_end_to_end_workflow.cpython-312-pytest-8.4.0.pyc b/tests/integration/__pycache__/test_end_to_end_workflow.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1af3f13593322313bbe8f541f293db239c5de907 Binary files /dev/null and b/tests/integration/__pycache__/test_end_to_end_workflow.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/integration/__pycache__/test_performance_resources.cpython-312-pytest-8.4.0.pyc b/tests/integration/__pycache__/test_performance_resources.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da8aaa645f09ee0eab26c8179fe4b5902cf9728d Binary files /dev/null and b/tests/integration/__pycache__/test_performance_resources.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/integration/__pycache__/test_ui_endpoints.cpython-312-pytest-8.4.0.pyc b/tests/integration/__pycache__/test_ui_endpoints.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ac7c1d41b74bb1d2936c3840b6fbacb9775d458 Binary files /dev/null and b/tests/integration/__pycache__/test_ui_endpoints.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0b048223c62e1204a82c1f01317c5512a3ba3dda --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests package.""" \ No newline at end of file diff --git a/tests/unit/__pycache__/__init__.cpython-312.pyc b/tests/unit/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..036808d0a9e7c8b39f659669c0daaa0102e96c94 Binary files /dev/null and b/tests/unit/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/unit/__pycache__/test_citation_formatter_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_citation_formatter_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdd4cd4baaad019799a342256a76f84000d94ec5 Binary files /dev/null and b/tests/unit/__pycache__/test_citation_formatter_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_code_generator_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_code_generator_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f115a0f09a3c665b1e4913ae9a54162daf6d4b72 Binary files /dev/null and b/tests/unit/__pycache__/test_code_generator_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_code_runner_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_code_runner_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9edaeef2521b6785992991631633e780d9bfa707 Binary files /dev/null and b/tests/unit/__pycache__/test_code_runner_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_llm_processor_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_llm_processor_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f17f88840945cdfbe71d51c2233a00b70021b81 Binary files /dev/null and b/tests/unit/__pycache__/test_llm_processor_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_orchestrator_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_orchestrator_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc19412033c665ef82d18a962967077c8d33e25e Binary files /dev/null and b/tests/unit/__pycache__/test_orchestrator_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_question_enhancer_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_question_enhancer_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40b5bc9b9cbaaaf847097f4219e09b3df991ffda Binary files /dev/null and b/tests/unit/__pycache__/test_question_enhancer_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_question_enhancer_isolated.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_question_enhancer_isolated.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fa0761f53202ac1cddc2b969277b8ba8ba631bf Binary files /dev/null and b/tests/unit/__pycache__/test_question_enhancer_isolated.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_utility_functions.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_utility_functions.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e22bcdf51b27c075e4d15c0a4bd0b1ef03c04028 Binary files /dev/null and b/tests/unit/__pycache__/test_utility_functions.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/__pycache__/test_web_search_agent.cpython-312-pytest-8.4.0.pyc b/tests/unit/__pycache__/test_web_search_agent.cpython-312-pytest-8.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84a8f95d64976d0a631211287c8cf3d9a8c07190 Binary files /dev/null and b/tests/unit/__pycache__/test_web_search_agent.cpython-312-pytest-8.4.0.pyc differ diff --git a/tests/unit/test_citation_formatter_agent.py b/tests/unit/test_citation_formatter_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..1ab4e6ada9885cef708acbbeada042dab7db537c --- /dev/null +++ b/tests/unit/test_citation_formatter_agent.py @@ -0,0 +1,65 @@ +"""Unit tests for CitationFormatterAgent - Simplified.""" + +import pytest +from unittest.mock import Mock + + +class MockCitationFormatterAgent: + """Mock implementation for testing.""" + + def format_citation(self, url: str): + """Mock format_citation method.""" + if not url or not url.startswith("http"): + return { + "status": "error", + "citation": "", + "error": "Invalid URL" + } + + return { + "status": "success", + "citation": f"Author, A. (2024). Title. Retrieved from {url}", + "title": "Sample Title", + "author": "Sample Author", + "year": "2024" + } + + +class TestCitationFormatterAgent: + """Test suite for CitationFormatterAgent.""" + + def setup_method(self): + """Set up test fixtures.""" + self.agent = MockCitationFormatterAgent() + + def test_format_citation_success(self): + """Test successful citation formatting.""" + # Setup + url = "https://example.com/article" + + # Execute + result = self.agent.format_citation(url) + + # Verify + assert result["status"] == "success" + assert "citation" in result + assert url in result["citation"] + assert "Author, A." in result["citation"] + + def test_format_citation_invalid_url(self): + """Test citation formatting with invalid URL.""" + # Execute + result = self.agent.format_citation("not-a-url") + + # Verify + assert result["status"] == "error" + assert "error" in result + + def test_format_citation_empty_url(self): + """Test citation formatting with empty URL.""" + # Execute + result = self.agent.format_citation("") + + # Verify + assert result["status"] == "error" + assert "error" in result \ No newline at end of file diff --git a/tests/unit/test_code_generator_agent.py b/tests/unit/test_code_generator_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..55df3ac41e0fdc28259e8e1619d45956aac2d759 --- /dev/null +++ b/tests/unit/test_code_generator_agent.py @@ -0,0 +1,89 @@ +"""Unit tests for CodeGeneratorAgent - Simplified.""" + +import pytest +from unittest.mock import Mock + + +class MockCodeGeneratorAgent: + """Mock implementation for testing.""" + + def generate_code(self, request: str, context: str = ""): + """Mock generate_code method.""" + if not request: + return { + "status": "error", + "code": "", + "error": "Empty request" + } + + # Simple mock code based on request + if "csv" in request.lower(): + code = "import pandas as pd\ndf = pd.read_csv('data.csv')\nprint(df.head())" + elif "plot" in request.lower(): + code = "import matplotlib.pyplot as plt\nplt.plot([1,2,3], [1,4,9])\nplt.show()" + else: + code = f"# Code for: {request}\nprint('Hello, World!')" + + return { + "status": "success", + "code": code, + "context_used": bool(context), + "dependencies": ["pandas"] if "csv" in request.lower() else [] + } + + +class TestCodeGeneratorAgent: + """Test suite for CodeGeneratorAgent.""" + + def setup_method(self): + """Set up test fixtures.""" + self.agent = MockCodeGeneratorAgent() + + def test_generate_code_success(self): + """Test successful code generation.""" + # Setup + request = "Create a script to read CSV data" + + # Execute + result = self.agent.generate_code(request) + + # Verify + assert result["status"] == "success" + assert "pandas" in result["code"] + assert "read_csv" in result["code"] + assert "pandas" in result["dependencies"] + + def test_generate_code_with_context(self): + """Test code generation with context.""" + # Setup + request = "Plot some data" + context = "We have numerical data in arrays" + + # Execute + result = self.agent.generate_code(request, context) + + # Verify + assert result["status"] == "success" + assert "matplotlib" in result["code"] + assert result["context_used"] is True + + def test_generate_code_empty_request(self): + """Test code generation with empty request.""" + # Execute + result = self.agent.generate_code("") + + # Verify + assert result["status"] == "error" + assert "error" in result + + def test_generate_code_generic(self): + """Test generic code generation.""" + # Setup + request = "Write a hello world program" + + # Execute + result = self.agent.generate_code(request) + + # Verify + assert result["status"] == "success" + assert "Hello, World!" in result["code"] \ No newline at end of file diff --git a/tests/unit/test_llm_processor_agent.py b/tests/unit/test_llm_processor_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..19370ce44fe31799b5c87e47bcbc77cbac28a426 --- /dev/null +++ b/tests/unit/test_llm_processor_agent.py @@ -0,0 +1,66 @@ +"""Unit tests for LLMProcessorAgent - Simplified.""" + +import pytest +from unittest.mock import Mock + + +class MockLLMProcessorAgent: + """Mock implementation for testing.""" + + def process(self, content: str, instruction: str = ""): + """Mock process method.""" + if not content: + return { + "status": "error", + "result": "", + "error": "Empty content" + } + + return { + "status": "success", + "result": f"Processed: {content[:50]}..." if len(content) > 50 else f"Processed: {content}", + "instruction_used": instruction + } + + +class TestLLMProcessorAgent: + """Test suite for LLMProcessorAgent.""" + + def setup_method(self): + """Set up test fixtures.""" + self.agent = MockLLMProcessorAgent() + + def test_process_success(self): + """Test successful content processing.""" + # Setup + content = "This is some content to process" + instruction = "Summarize this content" + + # Execute + result = self.agent.process(content, instruction) + + # Verify + assert result["status"] == "success" + assert "Processed:" in result["result"] + assert result["instruction_used"] == instruction + + def test_process_empty_content(self): + """Test processing with empty content.""" + # Execute + result = self.agent.process("", "summarize") + + # Verify + assert result["status"] == "error" + assert "error" in result + + def test_process_long_content(self): + """Test processing with long content.""" + # Setup + content = "This is a very long piece of content that should be truncated in the mock response to test handling of large text." + + # Execute + result = self.agent.process(content) + + # Verify + assert result["status"] == "success" + assert "..." in result["result"] # Should be truncated \ No newline at end of file diff --git a/tests/unit/test_question_enhancer_agent.py b/tests/unit/test_question_enhancer_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..7f5e10cc925b9b2a12205fc44da16b3af6e17532 --- /dev/null +++ b/tests/unit/test_question_enhancer_agent.py @@ -0,0 +1,56 @@ +"""Unit tests for QuestionEnhancerAgent.""" + +import pytest +from unittest.mock import Mock, patch, MagicMock +import json + +class MockQuestionEnhancerAgent: + """Mock implementation for testing.""" + + def enhance_question(self, user_request: str, num_questions: int = 3): + """Mock enhance_question method.""" + return { + "sub_questions": [ + f"Question {i+1} about {user_request[:20]}?" for i in range(num_questions) + ] + } + +class TestQuestionEnhancerAgent: + """Test suite for QuestionEnhancerAgent.""" + + def setup_method(self): + """Set up test fixtures.""" + self.agent = MockQuestionEnhancerAgent() + + def test_enhance_question_success(self): + """Test successful question enhancement.""" + # Setup + user_request = "How do I analyze CSV data with Python?" + + # Execute + result = self.agent.enhance_question(user_request, num_questions=3) + + # Verify + assert "sub_questions" in result + assert len(result["sub_questions"]) == 3 + assert all("Question" in q for q in result["sub_questions"]) + + def test_enhance_question_custom_num(self): + """Test question enhancement with custom number.""" + # Setup + user_request = "Create a web scraper" + + # Execute + result = self.agent.enhance_question(user_request, num_questions=5) + + # Verify + assert len(result["sub_questions"]) == 5 + + def test_enhance_question_empty_request(self): + """Test question enhancement with different inputs.""" + # Execute + result = self.agent.enhance_question("", num_questions=2) + + # Verify - should still work with empty string + assert len(result["sub_questions"]) == 2 + assert all(isinstance(q, str) for q in result["sub_questions"]) \ No newline at end of file diff --git a/tests/unit/test_web_search_agent.py b/tests/unit/test_web_search_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..60d14bb6c511bd2c0d994293ea02ee836f5426f9 --- /dev/null +++ b/tests/unit/test_web_search_agent.py @@ -0,0 +1,68 @@ +"""Unit tests for WebSearchAgent - Simplified.""" + +import pytest +from unittest.mock import Mock, patch, MagicMock + + +class MockWebSearchAgent: + """Mock implementation for testing.""" + + def search(self, query: str): + """Mock search method.""" + return { + "status": "success", + "results": [ + { + "title": f"Result for {query}", + "url": "https://example.com/1", + "content": f"Content about {query}", + "score": 0.9 + } + ], + "answer": f"Summary about {query}" + } + + +class TestWebSearchAgent: + """Test suite for WebSearchAgent.""" + + def setup_method(self): + """Set up test fixtures.""" + self.agent = MockWebSearchAgent() + + def test_search_basic_functionality(self): + """Test basic search functionality.""" + # Setup + query = "Python data analysis" + + # Execute + result = self.agent.search(query) + + # Verify + assert result["status"] == "success" + assert "results" in result + assert len(result["results"]) == 1 + assert result["results"][0]["title"] == "Result for Python data analysis" + assert "answer" in result + + def test_search_empty_query(self): + """Test search with empty query.""" + # Execute + result = self.agent.search("") + + # Verify - should still work + assert result["status"] == "success" + assert "results" in result + + def test_search_complex_query(self): + """Test search with complex query.""" + # Setup + query = "machine learning algorithms for beginners" + + # Execute + result = self.agent.search(query) + + # Verify + assert result["status"] == "success" + assert query in result["results"][0]["title"] + assert query in result["results"][0]["content"] \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..c7c3866a0bb184343cc42f4b4b2d0894685f7f53 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1902 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.12.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/ad/5b0f3451c2275af09966f1d7c0965facd4729a5b7efdc2eb728654679f85/aiohttp-3.12.9.tar.gz", hash = "sha256:2c9914c8914ff40b68c6e4ed5da33e88d4e8f368fddd03ceb0eb3175905ca782", size = 7810207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/2d/3234b91245a6f6cd0445c02604ac46c9e1d97cf50cfe421219533f061092/aiohttp-3.12.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7ae744b61b395e04b3d1acbbd301d98249397333f49419039517226ff32f3aa7", size = 698923 }, + { url = "https://files.pythonhosted.org/packages/63/d0/a81d09aea9d1aef10582c4d8fbc0158898ce2247f326a9c9922c9556212c/aiohttp-3.12.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d467a2049c4405853799dea41474b0ea9852fd465e7e2df819d3a33ac53214e8", size = 473547 }, + { url = "https://files.pythonhosted.org/packages/3b/ab/a282806eac098ddbd922038b1c2c5711ea4bb10fdb282f65986ae59c9096/aiohttp-3.12.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba7a8b5f02c2826eb29e8d6c38f1bc509efb506a2862131079b5b8d880ed4b62", size = 466383 }, + { url = "https://files.pythonhosted.org/packages/4d/2d/c6e796e6d7e57a3935772333d80e0407d66e551e2c7c2b930b7e18f527a4/aiohttp-3.12.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfe590ddb0dca3cdb601787079276545f00cfb9493f73f00fa011e71dae6f5fd", size = 1713182 }, + { url = "https://files.pythonhosted.org/packages/93/b7/bf9010f6dfe633147d74e93d41ec982b2538bfebcb6521a4139d187d07e3/aiohttp-3.12.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fc441aba05efec5c72127393f56206d0f3fb113aadcd1685033c10da1ff582ad", size = 1695833 }, + { url = "https://files.pythonhosted.org/packages/9e/b9/fe87b305d1a0272cb5c499402525c06571840349f2b2a4ffdc20e2996ac2/aiohttp-3.12.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a3f20a1b72643a0be5c9fcb97eb22607fcca32f1ca497f09a88d1ec3109daae", size = 1750928 }, + { url = "https://files.pythonhosted.org/packages/37/24/3ece3ca9c43b95a5836675c11f3be295fb65068ffffaad0e99a7a5b93c84/aiohttp-3.12.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3647dd1da43d595a52c5071b68fd8d39c0fd25b80f2cdd83eaabd9d59cd1f139", size = 1797083 }, + { url = "https://files.pythonhosted.org/packages/1c/d2/c153f7858d9c6db578b495b15f533182bd95f24c62ab125cc039d97bf588/aiohttp-3.12.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970bae350cedbabb7c9d0fc8564b004a547d4a27cf12dc986be0abf7d8cc8d81", size = 1716522 }, + { url = "https://files.pythonhosted.org/packages/1a/a9/ecfffc1659d8e3f02e109afec4df58a600128a2f48819af7e76a398a1ad3/aiohttp-3.12.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ccc5a5a4ccfa0ef0191dad2926e9752c37f368d846a70e40095a8529c5fb6eb", size = 1632325 }, + { url = "https://files.pythonhosted.org/packages/aa/07/69889c2e598661418f646038fc344769712a6dbc625c4b16f2d0191d872b/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:55197e86994682a332e8943eb01b462ae25630b10f245812e517251d7a922f25", size = 1693386 }, + { url = "https://files.pythonhosted.org/packages/c3/fb/23e292231a5d6d7413c998d096ed7dae049e7fb2c3406019eb04cb93c5b7/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:94d0cf6606ed9f2373565b8d0005bb070afbb81525ef6fa6e0725b8aec0c0843", size = 1714841 }, + { url = "https://files.pythonhosted.org/packages/80/bf/4d12162630ac2a39025c67bfeae94fdaeaec3b0438e65122f0012a570667/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0575d7ae9a9c206276a6aaa3ce364b467f29f0497c0db4449de060dc341d88d6", size = 1655490 }, + { url = "https://files.pythonhosted.org/packages/bc/a0/6c4f84197d9d04f548405d89d504afaef4c94dfea3842c52fa852f7f4c28/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9f44a4ebd717cc39796c4647495bc2901d0c168c71cd0132691ae3d0312215a9", size = 1735055 }, + { url = "https://files.pythonhosted.org/packages/aa/ae/6a9f1863e5d4b210890fb85b4b33e383351cc0588f1f30ea6866faef2141/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f9cdadfe84beb8ceafa98ab676e8c0caf1e5d60e8b33c385c11259ee0f7f2587", size = 1763027 }, + { url = "https://files.pythonhosted.org/packages/5e/8c/7c0ca97b65f38d3453cee496da8d465a7b0b44d302c6b5c1da4d83b62f1b/aiohttp-3.12.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:995b5640969b1250e37be6fc92d185e523e8df446f8bfa723b347e52d7ae80f9", size = 1722637 }, + { url = "https://files.pythonhosted.org/packages/4e/7b/9220a3c8d18398fa5195ece36970f71d8c5ba0b601c819b128dfe5171885/aiohttp-3.12.9-cp312-cp312-win32.whl", hash = "sha256:4cfa37e0797510fdb20ab0ee3ad483ae7cfacb27c6fb8de872a998705ad2286a", size = 420144 }, + { url = "https://files.pythonhosted.org/packages/f2/7e/adc99e6dd37bb2d762f4d78df3abd4635531e36bf489b4b580decb7166a1/aiohttp-3.12.9-cp312-cp312-win_amd64.whl", hash = "sha256:fdbd04e9b05885eaaefdb81c163b6dc1431eb13ee2da16d82ee980d4dd123890", size = 446243 }, + { url = "https://files.pythonhosted.org/packages/2b/5e/e7ee4927e72d65b68f612ca2013800c91aab38fd1f487926c2a8e4f1c8ea/aiohttp-3.12.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bf6fac88666d7e4c6cfe649d133fcedbc68e37a4472e8662d98a7cf576207303", size = 693344 }, + { url = "https://files.pythonhosted.org/packages/65/b5/f1dfda86a66913bfa9b7c3fe30d13f4d5a3642d3176ad0019968cda35d97/aiohttp-3.12.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:74e87ea6c832311b18a32b06baa6fee90a83dd630de951cca1aa175c3c9fa1ce", size = 471005 }, + { url = "https://files.pythonhosted.org/packages/09/e2/1502272a6e98665c71f9e996f126b64598c6e1660804eb4d78cad7ab3106/aiohttp-3.12.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16627b4caf6a36b605e3e1c4847e6d14af8e8d6b7dad322935be43237d4eb10d", size = 463304 }, + { url = "https://files.pythonhosted.org/packages/88/38/5c308d02754e346ca9eae63a086f438aae9a4fc36cdd1708fe41588b3883/aiohttp-3.12.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:998e323c107c3f6396c1f9de72289009057c611942771f24114ae78a76af0af5", size = 1702124 }, + { url = "https://files.pythonhosted.org/packages/ad/25/ab0af26f80c1b6035794d1c769d5671f7ecb59c93b64ea7dfced28df0dca/aiohttp-3.12.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:20f8a6d3af13f043a09726add6d096b533f180cf8b43970a8d9c9ca978bf45c5", size = 1683390 }, + { url = "https://files.pythonhosted.org/packages/23/fa/9a510d5ec8e1a75008a1c0e985e1db2ce339b9f82d838c7598b85f8f16d4/aiohttp-3.12.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bd0e06c8626361027f69df510c8484e17568ba2f91b2de51ea055f86ed3b071", size = 1735458 }, + { url = "https://files.pythonhosted.org/packages/0b/b2/870cabf883512f0f2cd9505bd7bce1e4574d137f132ab8d597ac5367b0ee/aiohttp-3.12.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64e22f12dd940a6e7b923637b10b611b752f6117bc3a780b7e61cc43c9e04892", size = 1784830 }, + { url = "https://files.pythonhosted.org/packages/68/cd/ab572264f5efbb8059f40d92d411918215bc4e669a7684bfa1ea0617745d/aiohttp-3.12.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b5bf453056b6ac4924ede1188d01e8b8d4801a6aa5351da3a7dbdbc03cb44e", size = 1707162 }, + { url = "https://files.pythonhosted.org/packages/19/6f/8a6a1dedb8ee5a4034e49bb3cb81ced4fe239d4d047f6bab538320fcb5bc/aiohttp-3.12.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00369db59f09860e0e26c75035f80f92881103e90f5858c18f29eb4f8cb8970f", size = 1620865 }, + { url = "https://files.pythonhosted.org/packages/ed/cf/6b7ab3b221a900a62e8cf26a47476377278675191aa2ea28327ba105c5c9/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:80fa1efc71d423be25db9dddefe8dcd90e487fbc9351a59549521b66405e71de", size = 1673887 }, + { url = "https://files.pythonhosted.org/packages/16/5c/aaa1fe022e86291c34a4e15e41d7cad589b4bdd66d473d6d537420763ab2/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5cade22a0f0a4665003ded2bc4d43bb69fde790e5a287187569509c33333a3ab", size = 1705551 }, + { url = "https://files.pythonhosted.org/packages/86/bf/0f7393a2ef0df4464945c3081d0629a9cb9bfaefaaa922dba225f7c47824/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d4a0fe3cd45cf6fb18222deef92af1c3efe090b7f43d477de61b2360c90a4b32", size = 1648148 }, + { url = "https://files.pythonhosted.org/packages/f9/71/286923ff54ae69c54e84bfbcc741b5833d980f192a93438f8d6cf153dae8/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:97b036ce251825fd5ab69d302ca8a99d3352af1c616cf40b2306fdb734cd6d30", size = 1724280 }, + { url = "https://files.pythonhosted.org/packages/58/48/808167d6f115165da3fcc6b7bb49bce6cc648471aa30634bcd47a7c96a32/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eeac3a965552dbf79bcc0b9b963b5f7d6364b1542eb609937278d70d27ae997f", size = 1757753 }, + { url = "https://files.pythonhosted.org/packages/c9/1b/949e7965d642cdd82c7d9576fd27c24b27f4e0e35586fceb81057a99f617/aiohttp-3.12.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a1f72b2560beaa949b5d3b324fc07b66846d39a8e7cc106ca450312a5771e3e", size = 1706642 }, + { url = "https://files.pythonhosted.org/packages/90/43/ea621cb45fc0e3e0a7906a1fdfd7a3176892c29e4e3d9d4dfa05159ac485/aiohttp-3.12.9-cp313-cp313-win32.whl", hash = "sha256:e429fce99ac3fd6423622713d2474a5911f24816ccdaf9a74c3ece854b7375c1", size = 419167 }, + { url = "https://files.pythonhosted.org/packages/ff/02/452bfb8285b980e463ca35c9d57b333a4defbb603983709dacfd27ca49a1/aiohttp-3.12.9-cp313-cp313-win_amd64.whl", hash = "sha256:ccb1931cc8b4dc6d7a2d83db39db18c3f9ac3d46a59289cea301acbad57f3d12", size = 445108 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anthropic" +version = "0.52.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/84/95126ee8df1acedd60bd03fe368d6335d65fe92e2c97581a81a82e8f576b/anthropic-0.52.2.tar.gz", hash = "sha256:9047bc960e8513950579c9cb730c16a84af3fcb56341ad7dc730772f83757050", size = 306204 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/3b/6f67f4e061d73cfaffc44dd41cbbe8b09efe1ec37b8135a2bcc043736d62/anthropic-0.52.2-py3-none-any.whl", hash = "sha256:00d52555f503e81e21aff0103db04cd93979cdf87ce8dd43c660ca6deae83ac6", size = 286262 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252 }, + { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183 }, + { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726 }, + { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718 }, + { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326 }, + { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539 }, + { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074 }, + { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210 }, + { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664 }, + { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255 }, + { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760 }, + { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992 }, + { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059 }, + { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412 }, + { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578 }, + { url = "https://files.pythonhosted.org/packages/7a/99/bb664a99561fd4266687e5cb8965e6ec31ba4ff7002c3fce3dc5ef2709db/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455", size = 46827 }, + { url = "https://files.pythonhosted.org/packages/c4/e3/f664171e867e0768ab982715e744430cf323f1282eb2e11ebfb6ee4c4551/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f", size = 27479 }, + { url = "https://files.pythonhosted.org/packages/a6/0d/2a79231ff54eb20e83b47e7610462ad6a2bea4e113fae5aa91c6547e7764/audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf", size = 27056 }, + { url = "https://files.pythonhosted.org/packages/86/46/342471398283bb0634f5a6df947806a423ba74b2e29e250c7ec0e3720e4f/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab", size = 87802 }, + { url = "https://files.pythonhosted.org/packages/56/44/7a85b08d4ed55517634ff19ddfbd0af05bf8bfd39a204e4445cd0e6f0cc9/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6", size = 95016 }, + { url = "https://files.pythonhosted.org/packages/a8/2a/45edbca97ea9ee9e6bbbdb8d25613a36e16a4d1e14ae01557392f15cc8d3/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543", size = 87394 }, + { url = "https://files.pythonhosted.org/packages/14/ae/832bcbbef2c510629593bf46739374174606e25ac7d106b08d396b74c964/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074", size = 84874 }, + { url = "https://files.pythonhosted.org/packages/26/1c/8023c3490798ed2f90dfe58ec3b26d7520a243ae9c0fc751ed3c9d8dbb69/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78", size = 88698 }, + { url = "https://files.pythonhosted.org/packages/2c/db/5379d953d4918278b1f04a5a64b2c112bd7aae8f81021009da0dcb77173c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb", size = 90401 }, + { url = "https://files.pythonhosted.org/packages/99/6e/3c45d316705ab1aec2e69543a5b5e458d0d112a93d08994347fafef03d50/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2", size = 91864 }, + { url = "https://files.pythonhosted.org/packages/08/58/6a371d8fed4f34debdb532c0b00942a84ebf3e7ad368e5edc26931d0e251/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a", size = 98796 }, + { url = "https://files.pythonhosted.org/packages/ee/77/d637aa35497e0034ff846fd3330d1db26bc6fd9dd79c406e1341188b06a2/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a", size = 94116 }, + { url = "https://files.pythonhosted.org/packages/1a/60/7afc2abf46bbcf525a6ebc0305d85ab08dc2d1e2da72c48dbb35eee5b62c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63", size = 91520 }, + { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482 }, + { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780 }, + { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918 }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876 }, + { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130 }, + { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176 }, + { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068 }, + { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328 }, + { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099 }, + { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314 }, + { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489 }, + { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366 }, + { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165 }, + { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548 }, + { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898 }, + { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171 }, + { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564 }, + { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719 }, + { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634 }, + { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824 }, + { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872 }, + { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179 }, + { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393 }, + { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194 }, + { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580 }, + { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734 }, + { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959 }, + { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024 }, + { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867 }, + { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096 }, + { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478 }, + { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255 }, + { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109 }, + { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268 }, + { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071 }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, +] + +[[package]] +name = "ffmpy" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/0d/5d2411c1db5d734fbbc547d1049c679536513cea2c97124b3b90228dfb41/ffmpy-0.6.0.tar.gz", hash = "sha256:332dd93198a162db61e527e866a04578d3713e577bfe68f2ed26ba9d09dbc948", size = 4955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/2f/932f05d6c63e206baf1cb8ad6034f6eac6fe8dfdae86a74044216d4987fc/ffmpy-0.6.0-py3-none-any.whl", hash = "sha256:c8369bf45f8bd5285ebad94c4a789a79e7af86eded74c1f8c36eccf57aaea58c", size = 5513 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "frozenlist" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/a812e2fe6cb3f6c6cfc8d0303bf1742f2286004e5ec41ac8c89cf68cdb54/frozenlist-1.6.2.tar.gz", hash = "sha256:effc641518696471cf4962e8e32050133bc1f7b2851ae8fd0cb8797dd70dc202", size = 43108 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/50/4632c944c57945cc1960e10ab8d6120cefb97bf923fd89052a3bcf8dc605/frozenlist-1.6.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48544d07404d7fcfccb6cc091922ae10de4d9e512c537c710c063ae8f5662b85", size = 85258 }, + { url = "https://files.pythonhosted.org/packages/3a/f4/5be5dbb219f341a4e996588e8841806c1df0c880c440c1171d143c83ce39/frozenlist-1.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ee0cf89e7638de515c0bb2e8be30e8e2e48f3be9b6c2f7127bca4a1f35dff45", size = 49620 }, + { url = "https://files.pythonhosted.org/packages/2a/fe/6697c1242126dc344840a43bffd5d5013cf5d61b272567f68025274622e1/frozenlist-1.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e084d838693d73c0fe87d212b91af80c18068c95c3d877e294f165056cedfa58", size = 48129 }, + { url = "https://files.pythonhosted.org/packages/b1/cb/aa09a825abeabb8165282f3f79cb3f130847486ee6427d72d742efa604d6/frozenlist-1.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d918b01781c6ebb5b776c18a87dd3016ff979eb78626aaca928bae69a640c3", size = 241513 }, + { url = "https://files.pythonhosted.org/packages/2c/a3/9c22011770ea8b423adf0e12ec34200cf68ff444348d6c7c3466acc6be53/frozenlist-1.6.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2892d9ab060a847f20fab83fdb886404d0f213f648bdeaebbe76a6134f0973d", size = 234019 }, + { url = "https://files.pythonhosted.org/packages/88/39/83c077661ba708d28859dc01d299c9272c9adeb4b9e58dba85da2271cb08/frozenlist-1.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbd2225d7218e7d386f4953d11484b0e38e5d134e85c91f0a6b0f30fb6ae25c4", size = 247035 }, + { url = "https://files.pythonhosted.org/packages/78/9f/7153e16e51ee8d660e907ef43c5a73882e3dc96582f70b00ece7d8a69b43/frozenlist-1.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b679187cba0a99f1162c7ec1b525e34bdc5ca246857544d16c1ed234562df80", size = 244126 }, + { url = "https://files.pythonhosted.org/packages/71/1f/e8e6b72f3b285f8a6cfe4c01d14c4bbbf477c40868c8386bd9617298c696/frozenlist-1.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bceb7bd48849d4b76eac070a6d508aa3a529963f5d9b0a6840fd41fb381d5a09", size = 224463 }, + { url = "https://files.pythonhosted.org/packages/69/b5/20ab79daba2e787c3426f6fa7bb2114edfcdffa4cfb2dd1c8e84f6964519/frozenlist-1.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b1b79ae86fdacc4bf842a4e0456540947abba64a84e61b5ae24c87adb089db", size = 240225 }, + { url = "https://files.pythonhosted.org/packages/02/46/5d2e14cec6f577426f53e8726f824028da55703a5a6b41c6eb7a3cdf1372/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c5c3c575148aa7308a38709906842039d7056bf225da6284b7a11cf9275ac5d", size = 237668 }, + { url = "https://files.pythonhosted.org/packages/5d/35/d29a3297954c34b69842f63541833eaca71e50fb6ebbafd9eb95babc1508/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16263bd677a31fe1a5dc2b803b564e349c96f804a81706a62b8698dd14dbba50", size = 248603 }, + { url = "https://files.pythonhosted.org/packages/1e/30/bcb572840d112b22b89d2178168741674ab3766ad507c33e2549fdfee7f0/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2e51b2054886ff7db71caf68285c2cd936eb7a145a509965165a2aae715c92a7", size = 225855 }, + { url = "https://files.pythonhosted.org/packages/ac/33/a0d3f75b126a18deb151f1cfb42ff64bbce22d8651fdda061e4fb56cd9b5/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ae1785b76f641cce4efd7e6f49ca4ae456aa230383af5ab0d4d3922a7e37e763", size = 246094 }, + { url = "https://files.pythonhosted.org/packages/4d/7c/c5140e62f1b878a2982246505ed9461c4238f17fd53237ae25ddc9dbeb8d/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:30155cc481f73f92f47ab1e858a7998f7b1207f9b5cf3b3cba90ec65a7f224f5", size = 247984 }, + { url = "https://files.pythonhosted.org/packages/77/da/32ac9c843ee126f8b2c3b164cf39a1bbf05e7a46e57659fef1db4f35e5dc/frozenlist-1.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1a1d82f2eb3d2875a8d139ae3f5026f7797f9de5dce44f53811ab0a883e85e7", size = 239770 }, + { url = "https://files.pythonhosted.org/packages/e0/2f/4c512f0f9db149609c7f7e7be108ddce93131bf56e81adddb64510919573/frozenlist-1.6.2-cp312-cp312-win32.whl", hash = "sha256:84105cb0f3479dfa20b85f459fb2db3b0ee52e2f84e86d447ea8b0de1fb7acdd", size = 40918 }, + { url = "https://files.pythonhosted.org/packages/54/c9/abb008594e5474132398aa417522776bee64d1753f98634c97b541938566/frozenlist-1.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:eecc861bd30bc5ee3b04a1e6ebf74ed0451f596d91606843f3edbd2f273e2fe3", size = 45148 }, + { url = "https://files.pythonhosted.org/packages/b8/f6/973abfcb8b68f2e8b58071a04ec72f5e1f0acd19dae0d3b7a8abc3d9ab07/frozenlist-1.6.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ad8851ae1f6695d735f8646bf1e68675871789756f7f7e8dc8224a74eabb9d0", size = 85517 }, + { url = "https://files.pythonhosted.org/packages/c8/d0/ac45f2dcf0afd5f7d57204af8b7516ecbc3599ea681e06f4b25d3845bea8/frozenlist-1.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd2d5abc0ccd99a2a5b437987f3b1e9c265c1044d2855a09ac68f09bbb8082ca", size = 49916 }, + { url = "https://files.pythonhosted.org/packages/50/cc/99c3f31823630b7411f7c1e83399e91d6b56a5661a5b724935ef5b51f5f5/frozenlist-1.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15c33f665faa9b8f8e525b987eeaae6641816e0f6873e8a9c4d224338cebbb55", size = 48107 }, + { url = "https://files.pythonhosted.org/packages/85/4e/38643ce3ee80d222892b694d02c15ea476c4d564493a6fe530347163744e/frozenlist-1.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e6c0681783723bb472b6b8304e61ecfcb4c2b11cf7f243d923813c21ae5d2a", size = 255771 }, + { url = "https://files.pythonhosted.org/packages/ca/e6/ceed85a7d5c0f666485384fc393e32353f8088e154a1109e5ef60165d366/frozenlist-1.6.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:61bae4d345a26550d0ed9f2c9910ea060f89dbfc642b7b96e9510a95c3a33b3c", size = 252519 }, + { url = "https://files.pythonhosted.org/packages/29/99/9f2e2b90cf918465e3b6ca4eea79e6be53d24fba33937e37d86c3764bbf9/frozenlist-1.6.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90e5a84016d0d2fb828f770ede085b5d89155fcb9629b8a3237c960c41c120c3", size = 263348 }, + { url = "https://files.pythonhosted.org/packages/4e/ac/59f3ec4c1b4897186efb4757379915734a48bb16bbc15a9fe0bf0857b679/frozenlist-1.6.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55dc289a064c04819d669e6e8a85a1c0416e6c601782093bdc749ae14a2f39da", size = 257858 }, + { url = "https://files.pythonhosted.org/packages/48/4a/19c97510d0c2be1ebaae68383d1b5a256a12a660ca17b0c427b1024d9b92/frozenlist-1.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b79bcf97ca03c95b044532a4fef6e5ae106a2dd863875b75fde64c553e3f4820", size = 238248 }, + { url = "https://files.pythonhosted.org/packages/ef/64/641aa2b0944fa3d881323948e0d8d6fee746dae03d9023eb510bb80bc46a/frozenlist-1.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e5e7564d232a782baa3089b25a0d979e2e4d6572d3c7231fcceacc5c22bf0f7", size = 255932 }, + { url = "https://files.pythonhosted.org/packages/6c/f8/5b68d5658fac7332e5d26542a4af0ffc2edca8da8f854f6274882889ee1e/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6fcd8d56880dccdd376afb18f483ab55a0e24036adc9a83c914d4b7bb5729d4e", size = 253329 }, + { url = "https://files.pythonhosted.org/packages/e9/20/379d7a27eb82748b41319bf376bf2c034e7ee11dda94f12b331edcc261ff/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4fbce985c7fe7bafb4d9bf647c835dbe415b465a897b0c79d1bdf0f3fae5fe50", size = 266164 }, + { url = "https://files.pythonhosted.org/packages/13/bd/d7dbf94220020850392cb661bedfdf786398bafae85d1045dd108971d261/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3bd12d727cd616387d50fe283abebb2db93300c98f8ff1084b68460acd551926", size = 241641 }, + { url = "https://files.pythonhosted.org/packages/a4/70/916fef6284d294077265cd69ad05f228e44f7ed88d9acb690df5a1174049/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:38544cae535ed697960891131731b33bb865b7d197ad62dc380d2dbb1bceff48", size = 261215 }, + { url = "https://files.pythonhosted.org/packages/8f/98/1326a7189fa519692698cddf598f56766b0fea6ac71cddaf64760a055397/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:47396898f98fae5c9b9bb409c3d2cf6106e409730f35a0926aad09dd7acf1ef5", size = 262597 }, + { url = "https://files.pythonhosted.org/packages/f4/d6/0a95ab9289c72e86c37c9b8afe82576556456b6f66a35d242526634130f2/frozenlist-1.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10d835f8ce8571fd555db42d3aef325af903535dad7e6faa7b9c8abe191bffc", size = 258766 }, + { url = "https://files.pythonhosted.org/packages/1b/d0/9e946aabd89ebfcb71ec1371327f0e25d4868cd4439471a6fcb6eaf7b366/frozenlist-1.6.2-cp313-cp313-win32.whl", hash = "sha256:a400fe775a41b6d7a3fef00d88f10cbae4f0074c9804e282013d7797671ba58d", size = 40961 }, + { url = "https://files.pythonhosted.org/packages/43/e9/d714f5eb0fde1413344ded982ae9638307b59651d5c04263af42eb81a315/frozenlist-1.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:cc8b25b321863ed46992558a29bb09b766c41e25f31461666d501be0f893bada", size = 46204 }, + { url = "https://files.pythonhosted.org/packages/f5/7a/8f6dde73862499e60eb390778a1e46b87c1fe3c5722622d731ccda7a173c/frozenlist-1.6.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:56de277a0e0ad26a1dcdc99802b4f5becd7fd890807b68e3ecff8ced01d58132", size = 91326 }, + { url = "https://files.pythonhosted.org/packages/79/60/dcdc75edbcf8241e7cb15fced68b3be63f67ff3faaf559c540a7eb63233b/frozenlist-1.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9cb386dd69ae91be586aa15cb6f39a19b5f79ffc1511371eca8ff162721c4867", size = 52426 }, + { url = "https://files.pythonhosted.org/packages/64/e6/df2a43ccb2c4f1ea3692aae9a89cfc5dd932a90b7898f98f13ed9e2680a9/frozenlist-1.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53835d8a6929c2f16e02616f8b727bd140ce8bf0aeddeafdb290a67c136ca8ad", size = 51460 }, + { url = "https://files.pythonhosted.org/packages/fd/b3/c4f2f7fca9487b25c39bf64535f029316e184072a82f3660ce72defc5421/frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc49f2277e8173abf028d744f8b7d69fe8cc26bffc2de97d47a3b529599fbf50", size = 310270 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/046eb34d8d0fee1a8c9dc91a9ba581283c67a1ace20bcc01c86a53595105/frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:65eb9e8a973161bdac5fa06ea6bd261057947adc4f47a7a6ef3d6db30c78c5b4", size = 289062 }, + { url = "https://files.pythonhosted.org/packages/48/7b/80991efaa0aa25e867cf93033c28e9d1310f34f90421eb59eb1f2073d937/frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:301eb2f898d863031f8c5a56c88a6c5d976ba11a4a08a1438b96ee3acb5aea80", size = 312202 }, + { url = "https://files.pythonhosted.org/packages/78/6b/6fe30bdababdf82c5b34f0093770c4be6211071e23570721b80b11c9d52a/frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207f717fd5e65fddb77d33361ab8fa939f6d89195f11307e073066886b33f2b8", size = 309557 }, + { url = "https://files.pythonhosted.org/packages/9d/ef/b7bf48802fc7d084703ba2173e6a8d0590bea378dcd6a480051c41bddf47/frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f83992722642ee0db0333b1dbf205b1a38f97d51a7382eb304ba414d8c3d1e05", size = 282135 }, + { url = "https://files.pythonhosted.org/packages/af/f8/6911a085bce8d0d0df3dfc2560e3e0fb4d6c19ff101014bcf61aa32ba39a/frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12af99e6023851b36578e5bcc60618b5b30f4650340e29e565cd1936326dbea7", size = 303392 }, + { url = "https://files.pythonhosted.org/packages/9c/5d/b4e0cc6dbd6b9282926a470a919da7c6599ff324ab5268c7ecaff82cb858/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6f01620444a674eaad900a3263574418e99c49e2a5d6e5330753857363b5d59f", size = 309402 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/bf777de3c810e68e8758337fcc97ee8c956376c87aecee9a61ba19a94123/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:82b94c8948341512306ca8ccc702771600b442c6abe5f8ee017e00e452a209e8", size = 312924 }, + { url = "https://files.pythonhosted.org/packages/0e/03/a69b890bc310790fcae61fd3b5be64876811b12db5d50b32e62f65e766bd/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:324a4cf4c220ddb3db1f46ade01e48432c63fa8c26812c710006e7f6cfba4a08", size = 291768 }, + { url = "https://files.pythonhosted.org/packages/70/cc/559386adf987b47c8977c929271d11a72efd92778a0a2f4cc97827a9a25b/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:695284e51458dabb89af7f7dc95c470aa51fd259207aba5378b187909297feef", size = 313305 }, + { url = "https://files.pythonhosted.org/packages/e7/fa/eb0e21730ffccfb2d0d367d863cbaacf8367bdc277b44eabf72f7329ab91/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:9ccbeb1c8dda4f42d0678076aa5cbde941a232be71c67b9d8ca89fbaf395807c", size = 312228 }, + { url = "https://files.pythonhosted.org/packages/d1/c1/8471b67172abc9478ad78c70a3f3a5c4fed6d4bcadc748e1b6dfa06ab2ae/frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cbbdf62fcc1864912c592a1ec748fee94f294c6b23215d5e8e9569becb7723ee", size = 309905 }, + { url = "https://files.pythonhosted.org/packages/bb/2c/ee21987c3a175b49d0b827b1e45394a7a5d08c7de5b766ed6d0889d30568/frozenlist-1.6.2-cp313-cp313t-win32.whl", hash = "sha256:76857098ee17258df1a61f934f2bae052b8542c9ea6b187684a737b2e3383a65", size = 44644 }, + { url = "https://files.pythonhosted.org/packages/65/46/fce60f65b1fb17a90c4bf410a5c90cb3b40616cc229e75866f8be97c112c/frozenlist-1.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c06a88daba7e891add42f9278cdf7506a49bc04df9b1648be54da1bf1c79b4c6", size = 50607 }, + { url = "https://files.pythonhosted.org/packages/13/be/0ebbb283f2d91b72beaee2d07760b2c47dab875c49c286f5591d3d157198/frozenlist-1.6.2-py3-none-any.whl", hash = "sha256:947abfcc8c42a329bbda6df97a4b9c9cdb4e12c85153b3b57b9d2f02aa5877dc", size = 12582 }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052 }, +] + +[[package]] +name = "gradio" +version = "5.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "ruff", marker = "sys_platform != 'emscripten'" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette", marker = "sys_platform != 'emscripten'" }, + { name = "tomlkit" }, + { name = "typer", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions" }, + { name = "urllib3", marker = "sys_platform == 'emscripten'" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/97/908eb543fbce7c69250d6fbe87b6ccf4ce397d31bceb360b40316357c68c/gradio-5.33.0.tar.gz", hash = "sha256:0cba3a1596fda6cb0048dd7ddc2d57e6238a047c0df9dee5a4a0e5c2a74e8e50", size = 64888401 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/c3/c9b09b8d7efd63d83a9c8d9c53b02e1b77238e14305a7ee561e0a8990465/gradio-5.33.0-py3-none-any.whl", hash = "sha256:165e412e1510a22471901744722f99a52cb56465a7e9609f1e400cac9999e9d8", size = 54208887 }, +] + +[package.optional-dependencies] +mcp = [ + { name = "mcp" }, + { name = "pydantic", marker = "sys_platform != 'emscripten'" }, +] + +[[package]] +name = "gradio-client" +version = "1.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/86/6684afe8691b024200fdc8983924f04b5f76bb401b9c700e5752a23595a0/gradio_client-1.10.2.tar.gz", hash = "sha256:bf71ba95714784fa77ca0cfb20189ad91c55e563c2dc71722d023a97f1815d7f", size = 321294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/1b/b372308c263379ae3ebc440512432979458330113bdee26cef86c89bf48e/gradio_client-1.10.2-py3-none-any.whl", hash = "sha256:6de67b6224123d264c7887caa0586b2a9e2c369ec32ca38927cf8a841694edcd", size = 323311 }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090 }, +] + +[[package]] +name = "grpclib" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h2" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/55936e462a5925190d7427e880b3033601d1effd13809b483d13a926061a/grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3", size = 61254 } + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + +[[package]] +name = "hf-xet" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/dc/dc091aeeb671e71cbec30e84963f9c0202c17337b24b0a800e7d205543e8/hf_xet-1.1.3.tar.gz", hash = "sha256:a5f09b1dd24e6ff6bcedb4b0ddab2d81824098bb002cf8b4ffa780545fa348c3", size = 488127 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/1f/bc01a4c0894973adebbcd4aa338a06815c76333ebb3921d94dcbd40dae6a/hf_xet-1.1.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c3b508b5f583a75641aebf732853deb058953370ce8184f5dabc49f803b0819b", size = 2256929 }, + { url = "https://files.pythonhosted.org/packages/78/07/6ef50851b5c6b45b77a6e018fa299c69a2db3b8bbd0d5af594c0238b1ceb/hf_xet-1.1.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b788a61977fbe6b5186e66239e2a329a3f0b7e7ff50dad38984c0c74f44aeca1", size = 2153719 }, + { url = "https://files.pythonhosted.org/packages/52/48/e929e6e3db6e4758c2adf0f2ca2c59287f1b76229d8bdc1a4c9cfc05212e/hf_xet-1.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2da210856444a34aad8ada2fc12f70dabed7cc20f37e90754d1d9b43bc0534", size = 4820519 }, + { url = "https://files.pythonhosted.org/packages/28/2e/03f89c5014a5aafaa9b150655f811798a317036646623bdaace25f485ae8/hf_xet-1.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8203f52827e3df65981984936654a5b390566336956f65765a8aa58c362bb841", size = 4964121 }, + { url = "https://files.pythonhosted.org/packages/47/8b/5cd399a92b47d98086f55fc72d69bc9ea5e5c6f27a9ed3e0cdd6be4e58a3/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30c575a5306f8e6fda37edb866762140a435037365eba7a17ce7bd0bc0216a8b", size = 5283017 }, + { url = "https://files.pythonhosted.org/packages/53/e3/2fcec58d2fcfd25ff07feb876f466cfa11f8dcf9d3b742c07fe9dd51ee0a/hf_xet-1.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c1a6aa6abed1f696f8099aa9796ca04c9ee778a58728a115607de9cc4638ff1", size = 4970349 }, + { url = "https://files.pythonhosted.org/packages/53/bf/10ca917e335861101017ff46044c90e517b574fbb37219347b83be1952f6/hf_xet-1.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:b578ae5ac9c056296bb0df9d018e597c8dc6390c5266f35b5c44696003cde9f3", size = 2310934 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "huggingface" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/168b574a23c1841fab5b24ecac98a88ea626ea3c746c481f79eb360c81f2/huggingface-0.0.1.tar.gz", hash = "sha256:0a2f228fd956801d68b7c6a8bef478dfa60c4b7d7eba572ea7de39ecf87e505a", size = 2320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/8c/e61fbc39c0a37140e1d4941c4af29e2d53bacf9f4559e3de24d8f4e484f0/huggingface-0.0.1-py3-none-any.whl", hash = "sha256:98a3409537557cd2fd768997ef94cab08529f86c5e106e6d54bbabdd5ee03910", size = 2455 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101 }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 }, + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866 }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772 }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534 }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087 }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694 }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992 }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723 }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215 }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762 }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427 }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mcp" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082 }, +] + +[[package]] +name = "mcp-hub-project" +version = "0.2.0" +source = { virtual = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "anthropic" }, + { name = "gradio", extra = ["mcp"] }, + { name = "gradio-client" }, + { name = "huggingface" }, + { name = "huggingface-hub" }, + { name = "modal" }, + { name = "openai" }, + { name = "psutil" }, + { name = "python-dotenv" }, + { name = "tavily-python" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.8.0" }, + { name = "anthropic", specifier = ">=0.52.2" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "gradio", extras = ["mcp"], specifier = ">=5.33.0" }, + { name = "gradio-client", specifier = ">=1.10.2" }, + { name = "huggingface", specifier = ">=0.0.1" }, + { name = "huggingface-hub", specifier = ">=0.32.4" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, + { name = "modal", specifier = ">=1.0.2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" }, + { name = "openai", specifier = ">=1.84.0" }, + { name = "psutil", specifier = ">=5.9.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "tavily-python", specifier = ">=0.7.4" }, +] +provides-extras = ["dev"] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "modal" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "click" }, + { name = "grpclib" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "synchronicity" }, + { name = "toml" }, + { name = "typer" }, + { name = "types-certifi" }, + { name = "types-toml" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/54/8d5d6090383698bd6c0d9612a1958c7b628260317063339a74f73a943e60/modal-1.0.2.tar.gz", hash = "sha256:f79cc1c75b378f1c92365a0a05741ac719af3d5900b67947c159c11df17d4ccc", size = 506313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/0b/5d6a775ec1901381b5a81e0c88db10efebd74f010bcae3613b418a46ae9c/modal-1.0.2-py3-none-any.whl", hash = "sha256:639bc4a0afa633f7a14355f730426c52627452b102af23358dcee36233abe958", size = 574253 }, +] + +[[package]] +name = "multidict" +version = "6.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 }, + { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 }, + { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 }, + { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 }, + { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 }, + { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 }, + { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 }, + { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 }, + { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 }, + { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 }, + { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 }, + { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 }, + { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 }, + { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 }, + { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 }, + { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 }, + { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 }, + { url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123 }, + { url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049 }, + { url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078 }, + { url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097 }, + { url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768 }, + { url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331 }, + { url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169 }, + { url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947 }, + { url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605 }, + { url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144 }, + { url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100 }, + { url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731 }, + { url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637 }, + { url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594 }, + { url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359 }, + { url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903 }, + { url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895 }, + { url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183 }, + { url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592 }, + { url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071 }, + { url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597 }, + { url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253 }, + { url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146 }, + { url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585 }, + { url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080 }, + { url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558 }, + { url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168 }, + { url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970 }, + { url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980 }, + { url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641 }, + { url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728 }, + { url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913 }, + { url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112 }, + { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 }, +] + +[[package]] +name = "mypy" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927 }, + { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082 }, + { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306 }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764 }, + { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233 }, + { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547 }, + { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753 }, + { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338 }, + { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764 }, + { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356 }, + { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745 }, + { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200 }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, +] + +[[package]] +name = "openai" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/a3/128caf24e116f48fad3e4d5122cdf84db06c5127911849d51663c66158c8/openai-1.84.0.tar.gz", hash = "sha256:4caa43bdab262cc75680ce1a2322cfc01626204074f7e8d9939ab372acf61698", size = 467066 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/10/f245db006a860dbc1f2e2c8382e0a1762c7753e7971ba43a1dc3f3ec1404/openai-1.84.0-py3-none-any.whl", hash = "sha256:7ec4436c3c933d68dc0f5a0cef0cb3dbc0864a54d62bddaf2ed5f3d521844711", size = 725512 }, +] + +[[package]] +name = "orjson" +version = "3.10.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 }, + { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 }, + { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 }, + { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 }, + { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 }, + { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 }, + { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 }, + { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 }, + { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 }, + { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 }, + { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 }, + { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 }, + { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 }, + { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 }, + { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 }, + { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 }, + { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 }, + { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 }, + { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 }, + { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 }, + { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 }, + { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 }, + { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 }, + { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 }, + { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 }, + { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 }, + { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "propcache" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, + { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, + { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, + { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, + { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, + { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, + { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, + { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, + { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, + { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, + { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, + { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, + { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, + { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, + { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, + { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, + { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865 }, + { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452 }, + { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800 }, + { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804 }, + { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235 }, + { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249 }, + { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964 }, + { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501 }, + { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917 }, + { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089 }, + { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102 }, + { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122 }, + { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818 }, + { url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112 }, + { url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034 }, + { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613 }, + { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763 }, + { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175 }, + { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265 }, + { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412 }, + { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290 }, + { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926 }, + { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808 }, + { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916 }, + { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384 }, + { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420 }, + { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880 }, + { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407 }, + { url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573 }, + { url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757 }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[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 } +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 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + +[[package]] +name = "ruff" +version = "0.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597 }, + { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154 }, + { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048 }, + { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062 }, + { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152 }, + { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067 }, + { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807 }, + { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261 }, + { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601 }, + { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186 }, + { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032 }, + { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529 }, + { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642 }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511 }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573 }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770 }, +] + +[[package]] +name = "safehttpx" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/4c/19db75e6405692b2a96af8f06d1258f8aa7290bdc35ac966f03e207f6d7f/safehttpx-0.1.6.tar.gz", hash = "sha256:b356bfc82cee3a24c395b94a2dbeabbed60aff1aa5fa3b5fe97c4f2456ebce42", size = 9987 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692 }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "sigtools" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/db/669ca14166814da187b3087b908ca924cf83f5b504fe23b3859a3ef67d4f/sigtools-4.0.1.tar.gz", hash = "sha256:4b8e135a9cd4d2ea00da670c093372d74e672ba3abb87f4c98d8e73dea54445c", size = 71910 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/91/853dbf6ec096197dba9cd5fd0c836c5fc19142038b7db60ebe6332b1bab1/sigtools-4.0.1-py2.py3-none-any.whl", hash = "sha256:d216b4cf920bbab0fce636ddc429ed8463a5b533d9e1492acb45a2a1bc36ac6c", size = 76419 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sse-starlette" +version = "2.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, +] + +[[package]] +name = "synchronicity" +version = "0.9.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sigtools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/2a/179088b83f2fc8ff8785f3f60c60435bb52467b1b97673d136dae8d46191/synchronicity-0.9.13.tar.gz", hash = "sha256:2e59a1aee4c9c57ab780e69ce8f1fb3f182a5b1234c43599be18ff8420e75892", size = 51144 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/8a/fd0a54e4bc7d09e6296efc7e9712f16e0001e82a13a90acb1e5729d34b39/synchronicity-0.9.13-py3-none-any.whl", hash = "sha256:762bb5f84def464b2c7dbb944d4f19c3336a12ed7046bc5d9646f0b4a3f6a0d5", size = 36974 }, +] + +[[package]] +name = "tavily-python" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/4a/1713e75df5a54639955b2a32e0a523fcb1ce50e8888570a494454c4d9e84/tavily_python-0.7.4.tar.gz", hash = "sha256:b448775485da3d2c99d6872df34073076379af6f6be72e0ae3ba89ae2540de1f", size = 17099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/49/938befade3324b1975404135207783a348c2da5682092193a788389f6c22/tavily_python-0.7.4-py3-none-any.whl", hash = "sha256:54037fbe4c0d54895139c262d450fdc6327e2869eff78f95b446a905b67df2ee", size = 15301 }, +] + +[[package]] +name = "tiktoken" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 }, + { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919 }, + { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877 }, + { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095 }, + { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649 }, + { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465 }, + { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669 }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, +] + +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 }, +] + +[[package]] +name = "types-toml" +version = "0.10.8.20240310" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, +] + +[[package]] +name = "watchfiles" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/8c/4f0b9bdb75a1bfbd9c78fad7d8854369283f74fe7cf03eb16be77054536d/watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2", size = 401511 }, + { url = "https://files.pythonhosted.org/packages/dc/4e/7e15825def77f8bd359b6d3f379f0c9dac4eb09dd4ddd58fd7d14127179c/watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f", size = 392715 }, + { url = "https://files.pythonhosted.org/packages/58/65/b72fb817518728e08de5840d5d38571466c1b4a3f724d190cec909ee6f3f/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec", size = 454138 }, + { url = "https://files.pythonhosted.org/packages/3e/a4/86833fd2ea2e50ae28989f5950b5c3f91022d67092bfec08f8300d8b347b/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21", size = 458592 }, + { url = "https://files.pythonhosted.org/packages/38/7e/42cb8df8be9a37e50dd3a818816501cf7a20d635d76d6bd65aae3dbbff68/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512", size = 487532 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/13d26721c85d7f3df6169d8b495fcac8ab0dc8f0945ebea8845de4681dab/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d", size = 522865 }, + { url = "https://files.pythonhosted.org/packages/a1/0d/7f9ae243c04e96c5455d111e21b09087d0eeaf9a1369e13a01c7d3d82478/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6", size = 499887 }, + { url = "https://files.pythonhosted.org/packages/8e/0f/a257766998e26aca4b3acf2ae97dff04b57071e991a510857d3799247c67/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234", size = 454498 }, + { url = "https://files.pythonhosted.org/packages/81/79/8bf142575a03e0af9c3d5f8bcae911ee6683ae93a625d349d4ecf4c8f7df/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2", size = 630663 }, + { url = "https://files.pythonhosted.org/packages/f1/80/abe2e79f610e45c63a70d271caea90c49bbf93eb00fa947fa9b803a1d51f/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663", size = 625410 }, + { url = "https://files.pythonhosted.org/packages/91/6f/bc7fbecb84a41a9069c2c6eb6319f7f7df113adf113e358c57fc1aff7ff5/watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249", size = 277965 }, + { url = "https://files.pythonhosted.org/packages/99/a5/bf1c297ea6649ec59e935ab311f63d8af5faa8f0b86993e3282b984263e3/watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705", size = 291693 }, + { url = "https://files.pythonhosted.org/packages/7f/7b/fd01087cc21db5c47e5beae507b87965db341cce8a86f9eb12bf5219d4e0/watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417", size = 283287 }, + { url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531 }, + { url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417 }, + { url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423 }, + { url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185 }, + { url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696 }, + { url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327 }, + { url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741 }, + { url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995 }, + { url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693 }, + { url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677 }, + { url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804 }, + { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "yarl" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089 }, + { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706 }, + { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719 }, + { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972 }, + { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639 }, + { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745 }, + { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178 }, + { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219 }, + { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266 }, + { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873 }, + { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524 }, + { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370 }, + { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297 }, + { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771 }, + { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355 }, + { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904 }, + { url = "https://files.pythonhosted.org/packages/0f/6f/514c9bff2900c22a4f10e06297714dbaf98707143b37ff0bcba65a956221/yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", size = 145030 }, + { url = "https://files.pythonhosted.org/packages/4e/9d/f88da3fa319b8c9c813389bfb3463e8d777c62654c7168e580a13fadff05/yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", size = 96894 }, + { url = "https://files.pythonhosted.org/packages/cd/57/92e83538580a6968b2451d6c89c5579938a7309d4785748e8ad42ddafdce/yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", size = 94457 }, + { url = "https://files.pythonhosted.org/packages/e9/ee/7ee43bd4cf82dddd5da97fcaddb6fa541ab81f3ed564c42f146c83ae17ce/yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", size = 343070 }, + { url = "https://files.pythonhosted.org/packages/4a/12/b5eccd1109e2097bcc494ba7dc5de156e41cf8309fab437ebb7c2b296ce3/yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", size = 337739 }, + { url = "https://files.pythonhosted.org/packages/7d/6b/0eade8e49af9fc2585552f63c76fa59ef469c724cc05b29519b19aa3a6d5/yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", size = 351338 }, + { url = "https://files.pythonhosted.org/packages/45/cb/aaaa75d30087b5183c7b8a07b4fb16ae0682dd149a1719b3a28f54061754/yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", size = 353636 }, + { url = "https://files.pythonhosted.org/packages/98/9d/d9cb39ec68a91ba6e66fa86d97003f58570327d6713833edf7ad6ce9dde5/yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", size = 348061 }, + { url = "https://files.pythonhosted.org/packages/72/6b/103940aae893d0cc770b4c36ce80e2ed86fcb863d48ea80a752b8bda9303/yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", size = 334150 }, + { url = "https://files.pythonhosted.org/packages/ef/b2/986bd82aa222c3e6b211a69c9081ba46484cffa9fab2a5235e8d18ca7a27/yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", size = 362207 }, + { url = "https://files.pythonhosted.org/packages/14/7c/63f5922437b873795d9422cbe7eb2509d4b540c37ae5548a4bb68fd2c546/yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", size = 361277 }, + { url = "https://files.pythonhosted.org/packages/81/83/450938cccf732466953406570bdb42c62b5ffb0ac7ac75a1f267773ab5c8/yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", size = 364990 }, + { url = "https://files.pythonhosted.org/packages/b4/de/af47d3a47e4a833693b9ec8e87debb20f09d9fdc9139b207b09a3e6cbd5a/yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", size = 374684 }, + { url = "https://files.pythonhosted.org/packages/62/0b/078bcc2d539f1faffdc7d32cb29a2d7caa65f1a6f7e40795d8485db21851/yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", size = 382599 }, + { url = "https://files.pythonhosted.org/packages/74/a9/4fdb1a7899f1fb47fd1371e7ba9e94bff73439ce87099d5dd26d285fffe0/yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", size = 378573 }, + { url = "https://files.pythonhosted.org/packages/fd/be/29f5156b7a319e4d2e5b51ce622b4dfb3aa8d8204cd2a8a339340fbfad40/yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", size = 86051 }, + { url = "https://files.pythonhosted.org/packages/52/56/05fa52c32c301da77ec0b5f63d2d9605946fe29defacb2a7ebd473c23b81/yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", size = 92742 }, + { url = "https://files.pythonhosted.org/packages/d4/2f/422546794196519152fc2e2f475f0e1d4d094a11995c81a465faf5673ffd/yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", size = 163575 }, + { url = "https://files.pythonhosted.org/packages/90/fc/67c64ddab6c0b4a169d03c637fb2d2a212b536e1989dec8e7e2c92211b7f/yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", size = 106121 }, + { url = "https://files.pythonhosted.org/packages/6d/00/29366b9eba7b6f6baed7d749f12add209b987c4cfbfa418404dbadc0f97c/yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", size = 103815 }, + { url = "https://files.pythonhosted.org/packages/28/f4/a2a4c967c8323c03689383dff73396281ced3b35d0ed140580825c826af7/yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", size = 408231 }, + { url = "https://files.pythonhosted.org/packages/0f/a1/66f7ffc0915877d726b70cc7a896ac30b6ac5d1d2760613603b022173635/yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", size = 390221 }, + { url = "https://files.pythonhosted.org/packages/41/15/cc248f0504610283271615e85bf38bc014224122498c2016d13a3a1b8426/yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", size = 411400 }, + { url = "https://files.pythonhosted.org/packages/5c/af/f0823d7e092bfb97d24fce6c7269d67fcd1aefade97d0a8189c4452e4d5e/yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", size = 411714 }, + { url = "https://files.pythonhosted.org/packages/83/70/be418329eae64b9f1b20ecdaac75d53aef098797d4c2299d82ae6f8e4663/yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", size = 404279 }, + { url = "https://files.pythonhosted.org/packages/19/f5/52e02f0075f65b4914eb890eea1ba97e6fd91dd821cc33a623aa707b2f67/yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", size = 384044 }, + { url = "https://files.pythonhosted.org/packages/6a/36/b0fa25226b03d3f769c68d46170b3e92b00ab3853d73127273ba22474697/yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", size = 416236 }, + { url = "https://files.pythonhosted.org/packages/cb/3a/54c828dd35f6831dfdd5a79e6c6b4302ae2c5feca24232a83cb75132b205/yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", size = 402034 }, + { url = "https://files.pythonhosted.org/packages/10/97/c7bf5fba488f7e049f9ad69c1b8fdfe3daa2e8916b3d321aa049e361a55a/yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", size = 407943 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/022d2555c1e8fcff08ad7f0f43e4df3aba34f135bff04dd35d5526ce54ab/yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", size = 423058 }, + { url = "https://files.pythonhosted.org/packages/4c/f6/0873a05563e5df29ccf35345a6ae0ac9e66588b41fdb7043a65848f03139/yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", size = 423792 }, + { url = "https://files.pythonhosted.org/packages/9e/35/43fbbd082708fa42e923f314c24f8277a28483d219e049552e5007a9aaca/yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", size = 422242 }, + { url = "https://files.pythonhosted.org/packages/ed/f7/f0f2500cf0c469beb2050b522c7815c575811627e6d3eb9ec7550ddd0bfe/yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", size = 93816 }, + { url = "https://files.pythonhosted.org/packages/3f/93/f73b61353b2a699d489e782c3f5998b59f974ec3156a2050a52dfd7e8946/yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", size = 101093 }, + { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 }, +]